diff --git a/CHANGELOG.md b/CHANGELOG.md index ae631ade..a7d6d007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +* `FlatList` has every function of the FSharp.Core `Array` module that applies to an immutable array, with its semantics + and exceptions, plus `add`, `asMemory`, `asSpan`, `binarySearch`, `binarySearchRange`, `binarySearchRangeWithComparer`, + `binarySearchWithComparer`, `indexOf`, `indexOfRange`, `indexOfRangeWith`, `indexOfWith`, `lastIndexOf`, + `lastIndexOfRange`, `lastIndexOfRangeWith`, `lastIndexOfWith`, `ofReadOnlySpan`, `ofSpan`, `remove`, `removeMany`, + `removeManyWith`, `removeWith`, `replace`, `replaceWith`, `skipLast` and `takeLast`. `remove` and `removeMany` remove by + value in `EqualityComparer<'T>.Default`, and the `With` variants take an equality comparer. The functions that wrap a + member of `ImmutableArray<'T>` throw what the member throws. +* The `FSharp.Collections.Immutable.Option.FlatList` and `FSharp.Collections.Immutable.Tuple.FlatList` modules repeat the + functions that use value options or struct tuples with options and reference tuples. +* The `FlatListBuilder` module works on the builder of a flat list: `add`, `addRange`, `blit`, `capacity`, `clear`, + `contains`, `drainToFlatList`, `indexOf`, `indexOfRange`, `indexOfRangeWith`, `insertAt`, `insertManyAt`, `item`, + `lastIndexOf`, `lastIndexOfRange`, `lastIndexOfRangeWith`, `length`, `remove`, `removeAll`, `removeAt`, `removeMany`, + `removeManyAt`, `removeManyWith`, `removeWith`, `replace`, `replaceWith`, `rev`, `setCapacity`, `sort`, `sortRange`, + `sortRangeWith`, `sortRangeWithComparer`, `sortWith`, `sortWithComparer`, `toArray` and `updateAt`. The functions that + change the builder return it. `replace` and `replaceWith` leave the builder as it is when no element equals the old + value, as `ImmutableArray<'T>.Builder.Replace` does, where `FlatList.replace` throws `ArgumentException`. +* `flatList { }` builds a flat list. + ### Changed * The package targets `net8.0` and `net10.0`. * The package requires FSharp.Core 10.1.301 or later, the first release with `partitionWith`. +* `FlatList`: the `try*` functions return `voption`; `choose`, `pick` and `tryPick` take functions that return `voption`; + the functions that return tuples return struct tuples. +* `FlatList`: every function throws what the FSharp.Core `Array` function of the same name throws. `take` and `splitAt` + throw `InvalidOperationException` for a count beyond the length, where they threw `ArgumentOutOfRangeException`. A + default `ImmutableArray` throws `ArgumentNullException`, where it threw `ArgumentException`, and also when it is given + as a sequence, where it threw `InvalidOperationException`. +* `FlatList.sub` has the meaning of `Array.sub`: `sub array startIndex count` returns `count` elements from `startIndex`. + It was `sub start stop list`. +* The nested `FlatList.Builder` module is now the `FlatListBuilder` module. + +### Removed + +* `FlatList.index`, `indexWith`, `lastIndex` and `lastIndexWith`: use `indexOf`, `indexOfWith`, `lastIndexOf` and + `lastIndexOfWith`. They compare in `EqualityComparer<'T>.Default`, where the removed functions compared in F# structural + equality: elements that are arrays, or types whose `Equals` differs from F# equality, compare differently. +* `FlatList.indexRange`, `indexRangeWith`, `lastIndexRange` and `lastIndexRangeWith`: use `indexOfRange`, + `indexOfRangeWith`, `lastIndexOfRange` and `lastIndexOfRangeWith`, which take the same arguments and compare as + `indexOf` does. +* `FlatList.indexFrom`, `indexFromWith`, `lastIndexFrom` and `lastIndexFromWith`: use `indexOfRange` and + `indexOfRangeWith` with the count `length - startIndex`, and `lastIndexOfRange` and `lastIndexOfRangeWith` with the + count `startIndex + 1`. +* `FlatList.removeAll` and `removeAllWith`: use `removeMany` and `removeManyWith`. They remove one element for each of the + given values, as the removed functions did, and `removeMany` compares in `EqualityComparer<'T>.Default` where `removeAll` + compared in F# structural equality. The removed functions did not remove the elements that satisfy a predicate: use + `filter` with the negated predicate for that. +* `FlatList.removeRange`: use `removeManyAt`. ## [1.0.0] - 2020-02-23 diff --git a/README.md b/README.md index 58e776c9..a10138fc 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | Module | .NET type | Computation expression | |---|---|---| -| `FlatList` | `ImmutableArray<'T>` | – | +| `FlatList` | `ImmutableArray<'T>` | `flatList { }` | | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index c568bf59..822820fa 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -26,7 +26,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | Module | .NET type | Computation expression | |---|---|---| -| `FlatList` | `ImmutableArray<'T>` | – | +| `FlatList` | `ImmutableArray<'T>` | `flatList { }` | | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | diff --git a/docsSrc/index.md b/docsSrc/index.md index 39c16874..276ad8fe 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -8,7 +8,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | Module | .NET type | Computation expression | |---|---|---| -| `FlatList` | `ImmutableArray<'T>` | – | +| `FlatList` | `ImmutableArray<'T>` | `flatList { }` | | `ImmutableList` | `ImmutableList<'T>` | – | | `Stack` | `ImmutableStack<'T>` | – | | `Queue` | `ImmutableQueue<'T>` | – | diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs new file mode 100644 index 00000000..6db0b059 --- /dev/null +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -0,0 +1,6 @@ +[] +module FSharp.Collections.Immutable.ComputationExpressions + +// ---- Creation ---- + +let flatList = FlatListCE () diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi new file mode 100644 index 00000000..c0086e56 --- /dev/null +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -0,0 +1,21 @@ +/// +/// The computation expressions of the collections (ADR 0001, D7). +/// +[] +module FSharp.Collections.Immutable.ComputationExpressions + +// ---- Creation ---- + +/// Builds a flat list. +/// +/// +/// let squares = flatList { +/// yield 0 +/// for i in 1..3 do +/// yield i * i +/// yield! [ 100; 200 ] +/// } +/// +/// Evaluates to a flat list of 0, 1, 4, 9, 100 and 200. +/// +val flatList : FlatListCE diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index f460764a..8e3efc2d 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -21,14 +21,25 @@ - + + + + + + + + + + + - + + diff --git a/src/FSharp.Collections.Immutable/FlatList.fs b/src/FSharp.Collections.Immutable/FlatList.fs deleted file mode 100644 index b6153265..00000000 --- a/src/FSharp.Collections.Immutable/FlatList.fs +++ /dev/null @@ -1,593 +0,0 @@ -#if INTERACTIVE -namespace global -#else -namespace FSharp.Collections.Immutable -#endif - -// The FlatList name comes from a similar construct seen in the official F# source code -type FlatList<'T> = System.Collections.Immutable.ImmutableArray<'T> - -// based on the F# Array module source -[] -module FlatList = - - type internal FlatListFactory = System.Collections.Immutable.ImmutableArray - - let inline internal checkNotDefault argName (list : FlatList<'T>) = - if list.IsDefault then - invalidArg argName "Uninstantiated ImmutableArray/FlatList" - - let inline internal check (list : FlatList<'T>) = checkNotDefault (nameof list) list - - ////////// Creating ////////// - - let inline empty<'T> : FlatList<_> = FlatListFactory.Create<'T>() - let inline singleton<'T> (item : 'T) : FlatList<'T> = FlatListFactory.Create<'T>(item) - - let inline ofSeq source = FlatListFactory.CreateRange source - let inline ofArray (source : _ array) = FlatListFactory.CreateRange source - - let inline toSeq (flatList : FlatList<_>) = flatList :> seq<_> - - let inline toArray (list : FlatList<_>) = - check list - Seq.toArray list - - ////////// Building ////////// - - let moveFromBuilder (builder : FlatList<_>.Builder) : FlatList<_> = - checkNotNull (nameof builder) builder - builder.MoveToImmutable () - - let ofBuilder (builder : FlatList<_>.Builder) : FlatList<_> = - checkNotNull (nameof builder) builder - builder.ToImmutable () - - let inline builder () : FlatList<'T>.Builder = FlatListFactory.CreateBuilder () - let inline builderWith capacity : FlatList<'T>.Builder = FlatListFactory.CreateBuilder (capacity) - - let toBuilder list : FlatList<_>.Builder = - check list - list.ToBuilder () - - module Builder = - let inline private check (builder : FlatList<'T>.Builder) = checkNotNull (nameof builder) builder - - let add item builder = - check builder - builder.Add (item) - - let inline internal indexNotFound () = raise <| System.Collections.Generic.KeyNotFoundException () - - let isEmpty (list : FlatList<_>) = list.IsEmpty - let isDefault (list : FlatList<_>) = list.IsDefault - let isDefaultOrEmpty (list : FlatList<_>) = list.IsDefaultOrEmpty - - ////////// IReadOnly* ////////// - - let length list = - check list - list.Length - - let item index list = - check list - list.[index] - - let append list1 list2 : FlatList<'T> = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - list1.AddRange (list2 : FlatList<_>) - - /// Searches for the specified object and returns the zero-based index of the first occurrence within the range - /// of elements in the list that starts at the specified index and - /// contains the specified number of elements. - let indexRangeWith comparer index count item list = - check list - list.IndexOf (item, index, count, comparer) - - let indexRange index count item list = indexRangeWith HashIdentity.Structural index count item list - let indexFromWith comparer index item list = indexRangeWith comparer index (length list - index) item - let indexFrom index item list = indexFromWith HashIdentity.Structural index item list - let indexWith comparer item list = indexFromWith comparer 0 item list - let index item list = indexWith HashIdentity.Structural item list - - /// Searches for the specified object and returns the zero-based index of the last occurrence within the - /// range of elements in the list that contains the specified number - /// of elements and ends at the specified index. - let lastIndexRangeWith comparer index count item list = - check list - list.LastIndexOf (item, index, count, comparer) - - let lastIndexRange index count item list = lastIndexRangeWith HashIdentity.Structural index count item list - let lastIndexFromWith comparer index item list = lastIndexRangeWith comparer index (index + 1) item list - let lastIndexFrom index item list = lastIndexFromWith HashIdentity.Structural index item list - let lastIndexWith comparer item list = lastIndexFromWith comparer (length list - 1) item list - let lastIndex item list = lastIndexWith HashIdentity.Structural item list - - /// Removes the specified objects from the list with the given comparer. - let removeAllWith (comparer : System.Collections.Generic.IEqualityComparer<'T>) (items : 'T seq) list : FlatList<_> = - check list - list.RemoveRange (items, comparer) - - /// Removes the specified objects from the list. - let removeAll items list = removeAllWith HashIdentity.Structural items list - - /// Removes all the elements that do not match the conditions defined by the specified predicate. - let filter predicate list : FlatList<_> = - check list - System.Predicate (not << predicate) |> list.RemoveAll - - /// Removes all the elements that do not match the conditions defined by the specified predicate. - let where predicate list = filter predicate list - - /// Removes a range of elements from the list. - let removeRange index (count : int) list : FlatList<_> = - check list - list.RemoveRange (index, count) - - let blit source sourceIndex (destination : 'T[]) destinationIndex count = - checkNotDefault (nameof source) source - - try - source.CopyTo (sourceIndex, destination, destinationIndex, count) - with exn -> - raise exn // throw same exception with the correct stack trace. Update exception code - - let sortRangeWithComparer comparer index count list = - check list - list.Sort (index, count, comparer) - - let sortRangeWith comparer index count list = - sortRangeWithComparer (ComparisonIdentity.FromFunction comparer) index count list - - let sortRange index count list = sortRangeWithComparer ComparisonIdentity.Structural index count list - - let sortWithComparer (comparer : System.Collections.Generic.IComparer<_>) list = - check list - list.Sort (comparer) - - let sortWith comparer list = sortWithComparer (ComparisonIdentity.FromFunction comparer) list - - let sort list = - check list - list.Sort () - - ////////// Loop-based ////////// - - let inline private builderWithLengthOf list = builderWith <| length list - - let init count initializer = - if count < 0 then - invalidArg (nameof count) ErrorStrings.InputMustBeNonNegative - - let builder = builderWith count - - for i = 0 to count - 1 do - builder.Add <| initializer i - - moveFromBuilder builder - - let rec private concatAddLengths (arrs : FlatList>) i acc = - if i >= length arrs then - acc - else - concatAddLengths arrs (i + 1) (acc + arrs.[i].Length) - - let concat (arrs : FlatList>) = // consider generalizing - let result : FlatList<'T>.Builder = builderWith <| concatAddLengths arrs 0 0 - - for i = 0 to length arrs - 1 do - result.AddRange (arrs.[i] : FlatList<'T>) - - moveFromBuilder result - - let inline map mapping list = - check list - let builder = builderWithLengthOf list - - for i = 0 to length list - 1 do - builder.Add (mapping list.[i]) - - moveFromBuilder builder - - let countBy projection list = - check list - // need struct box optimization - let dict = new System.Collections.Generic.Dictionary<'Key, int> (HashIdentity.Structural) - - // Build the groupings - for v in list do - let key = projection v - let mutable prev = Unchecked.defaultof<_> - - if dict.TryGetValue (key, &prev) then - dict.[key] <- prev + 1 - else - dict.[key] <- 1 - - let res = builderWith dict.Count - let mutable i = 0 - - for group in dict do - res.Add (group.Key, group.Value) - i <- i + 1 - - moveFromBuilder res - - let indexed list = - check list - let builder = builderWithLengthOf list - - for i = 0 to length list - 1 do - builder.Add (i, list.[i]) - - moveFromBuilder builder - - let inline iter action list = - check list - - for i = 0 to length list - 1 do - action list.[i] - - let iter2 action list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<'T, 'U, unit>.Adapt(action) - let len = length list1 - - if len <> length list2 then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - for i = 0 to len - 1 do - f.Invoke (list1.[i], list2.[i]) - - let distinctBy projection (list : FlatList<'T>) = - let builder : FlatList<'T>.Builder = builderWith <| length list - let set = System.Collections.Generic.HashSet<'Key>(HashIdentity.Structural) - let mutable outputIndex = 0 - - for i = 0 to length list - 1 do - let item = list.[i] - - if set.Add <| projection item then - outputIndex <- outputIndex + 1 - Builder.add item builder - - ofBuilder builder - - let map2 mapping list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) - let len1 = list1.Length - - if len1 <> list2.Length then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - let res = builderWith len1 - - for i = 0 to len1 - 1 do - res.Add <| f.Invoke (list1.[i], list2.[i]) - - moveFromBuilder res - - let map3 mapping list1 list2 list3 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - checkNotDefault (nameof list3) list3 - let f = OptimizedClosures.FSharpFunc<_, _, _, _>.Adapt(mapping) - let len1 = list1.Length - - if not (len1 = list2.Length) then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - if not (len1 = list3.Length) then - invalidArg (nameof list3) ErrorStrings.ListsHaveDifferentLengths - - let res = builderWith len1 - - for i = 0 to len1 - 1 do - res.Add <| f.Invoke (list1.[i], list2.[i], list3.[i]) - - moveFromBuilder res - - let mapi2 mapping list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<_, _, _, _>.Adapt(mapping) - let len1 = list1.Length - - if len1 <> list2.Length then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - let res = builderWith len1 - - for i = 0 to len1 - 1 do - res.Add <| f.Invoke (i, list1.[i], list2.[i]) - - moveFromBuilder res - - let iteri action list = - check list - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(action) - let len = list.Length - - for i = 0 to len - 1 do - f.Invoke (i, list.[i]) - - let iteri2 action list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<_, _, _, _>.Adapt(action) - let len1 = list1.Length - - if len1 <> list2.Length then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - for i = 0 to len1 - 1 do - f.Invoke (i, list1.[i], list2.[i]) - - let mapi mapping list = - check list - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(mapping) - let len = list.Length - let res = builderWithLengthOf list - - for i = 0 to len - 1 do - res.Add <| f.Invoke (i, list.[i]) - - moveFromBuilder res - - let exists predicate list = - check list - let len = list.Length - let rec loop i = i < len && (predicate list.[i] || loop (i + 1)) - loop 0 - - let inline contains e list = - check list - let mutable state = false - let mutable i = 0 - - while (not state && i < list.Length) do - state <- e = list.[i] - i <- i + 1 - - state - - let exists2 predicate list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(predicate) - let len1 = list1.Length - - if len1 <> list2.Length then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - let rec loop i = - i < len1 - && (f.Invoke (list1.[i], list2.[i]) || loop (i + 1)) - - loop 0 - - let forall predicate list = - check list - let len = list.Length - let rec loop i = i >= len || (predicate list.[i] && loop (i + 1)) - loop 0 - - let forall2 predicate list1 list2 = - checkNotDefault (nameof list1) list1 - checkNotDefault (nameof list2) list2 - let f = OptimizedClosures.FSharpFunc<_, _, _>.Adapt(predicate) - let len1 = list1.Length - - if len1 <> list2.Length then - invalidArg (nameof list2) ErrorStrings.ListsHaveDifferentLengths - - let rec loop i = - i >= len1 - || (f.Invoke (list1.[i], list2.[i]) && loop (i + 1)) - - loop 0 - - let groupBy projection list = - check list - let dict = new System.Collections.Generic.Dictionary<'Key, ResizeArray<'T>> (HashIdentity.Structural) - - // Build the groupings - for i = 0 to (list.Length - 1) do - let v = list.[i] - let key = projection v - let ok, prev = dict.TryGetValue (key) - - if ok then - prev.Add (v) - else - let prev = new ResizeArray<'T> (1) - dict.[key] <- prev - prev.Add (v) - - // Return the list-of-lists. - let result = builderWith dict.Count - let mutable i = 0 - - for group in dict do - result.Add (group.Key, ofSeq group.Value) - i <- i + 1 - - moveFromBuilder result - - let pick chooser list = - check list - - let rec loop i = - if i >= list.Length then - indexNotFound () - else - match chooser list.[i] with - | None -> loop (i + 1) - | Some res -> res - - loop 0 - - let tryPick chooser list = - check list - - let rec loop i = - if i >= list.Length then - None - else - match chooser list.[i] with - | None -> loop (i + 1) - | res -> res - - loop 0 - - let choose chooser list = - check list - let res = builderWith list.Length - - for i = 0 to list.Length - 1 do - match chooser list.[i] with - | None -> () - | Some b -> res.Add (b) - - ofBuilder res - - let partition predicate list = - check list - let res1 = builderWith list.Length - let res2 = builderWith list.Length - - for i = 0 to list.Length - 1 do - let x = list.[i] - if predicate x then res1.Add (x) else res2.Add (x) - - ofBuilder res1, ofBuilder res2 - - let find predicate list = - check list - - let rec loop i = - if i >= list.Length then indexNotFound () - else if predicate list.[i] then list.[i] - else loop (i + 1) - - loop 0 - - let tryFind predicate list = - check list - - let rec loop i = - if i >= list.Length then None - else if predicate list.[i] then Some list.[i] - else loop (i + 1) - - loop 0 - - let findBack predicate list = - check list - - let rec loop i = - if i < 0 then indexNotFound () - else if predicate list.[i] then list.[i] - else loop (i - 1) - - loop <| length list - 1 - - let tryFindBack predicate list = - check list - - let rec loop i = - if i < 0 then None - else if predicate list.[i] then Some list.[i] - else loop (i + 1) - - loop <| length list - 1 - - let findIndexBack predicate list = - check list - - let rec loop i = - if i < 0 then indexNotFound () - else if predicate list.[i] then i - else loop (i - 1) - - loop <| length list - 1 - - let tryFindIndexBack predicate list = - check list - - let rec loop i = - if i < 0 then None - else if predicate list.[i] then Some i - else loop (i - 1) - - loop <| length list - 1 - // TODO: windowed - - ////////// Based on other operations ////////// - - let take count list = removeRange count (length list - count) list - - let inline private lengthWhile predicate list = - check list - let mutable count = 0 - - while count < list.Length && predicate list.[count] do - count <- count + 1 - - count - - let takeWhile predicate list = take (lengthWhile predicate list) list - - let skip index list = removeRange 0 index list - - let skipWhile predicate list = skip (lengthWhile predicate list) list - - let sub start stop list = skip start list |> take (stop - start - 1) - - let truncate count list = if count < length list then take count list else list - - let splitAt index list = take index list, skip index list - - let head list = item 0 list - - let tryItem index list = - if index >= length list || index < 0 then - None - else - Some (list.[index]) - - let tryHead list = tryItem 0 list - - let last (list : FlatList<_>) = list.[length list - 1] - - let tryLast list = tryItem (length list - 1) list - - let tail list = removeRange 1 (length list - 1) list - - let tryTail list = if isEmpty list then None else Some <| tail list - - let create count item = init count <| fun _ -> item // optimize - - let replicate count item = create item count - - let collect mapping list = concat <| map mapping list - - let inline build f = - let builder = builder () - f builder - moveFromBuilder builder - - let inline update f list = - let builder = toBuilder list - f builder - moveFromBuilder builder - -////////// - -module ImmutableArray = FlatList diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fs b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs new file mode 100644 index 00000000..9ac7375d --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fs @@ -0,0 +1,1008 @@ +[] +module FSharp.Collections.Immutable.FlatList + +open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Runtime.InteropServices + +// Most functions unwrap the underlying array without a copy, call the FSharp.Core Array function, and wrap its fresh +// result without a copy (ADR 0001, D5), so that they have the semantics and the exceptions of the Array module (D6). + +// ---- Creation ---- + +[] +let create count (value : 'T) = Array.create count value |> wrapArray + +[] +[] +let empty<'T> : FlatList<'T> = ImmutableArray<'T>.Empty + +[] +let init count (initializer : int -> 'T) = Array.init count initializer |> wrapArray + +[] +let ofArray (array : 'T array) = Array.copy array |> wrapArray + +[] +let ofList (list : 'T list) = Array.ofList list |> wrapArray + +[] +let ofReadOnlySpan (span : ReadOnlySpan<'T>) = ImmutableArray.Create<'T> span + +[] +let ofSeq (source : seq<'T>) = + match source with + // An immutable array needs no copy; a default one is a default FlatList, which no function accepts (ADR 0001, D6) + | :? ImmutableArray<'T> as array -> + checkNotDefault (nameof source) array + array + | _ -> + // A default flat list of a derived type is also a sequence of its base type, and Array.ofSeq would fail on it + checkNotNullOrDefaultSeq (nameof source) source + Array.ofSeq source |> wrapArray + +[] +let ofSpan (span : Span<'T>) = ImmutableArray.Create<'T> span + +[] +let replicate count (initial : 'T) = Array.replicate count initial |> wrapArray + +[] +let singleton (value : 'T) = ImmutableArray.Create<'T> value + +[] +let unfold (generator : 'State -> struct ('T * 'State) voption) (state : 'State) = + let builder = ImmutableArray.CreateBuilder<'T>() + let mutable state = state + let mutable go = true + + while go do + match generator state with + | ValueSome (struct (item, next)) -> + builder.Add item + state <- next + | ValueNone -> go <- false + + builder.DrainToImmutable () + +// ---- Builder ---- + +[] +let build (action : FlatList<'T>.Builder -> unit) = + let builder = ImmutableArray.CreateBuilder<'T>() + action builder + builder.DrainToImmutable () + +[] +let builder () : FlatList<'T>.Builder = ImmutableArray.CreateBuilder<'T>() + +[] +let builderWith (capacity : int) : FlatList<'T>.Builder = ImmutableArray.CreateBuilder<'T> capacity + +[] +let moveFromBuilder (builder : FlatList<'T>.Builder) = + checkNotNull (nameof builder) builder + builder.MoveToImmutable () + +[] +let ofBuilder (builder : FlatList<'T>.Builder) = + checkNotNull (nameof builder) builder + builder.ToImmutable () + +[] +let toBuilder (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.ToBuilder () + +[] +let update (action : FlatList<'T>.Builder -> unit) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + let builder = array.ToBuilder () + action builder + builder.DrainToImmutable () + +// ---- Properties ---- + +[] +let isEmpty (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.IsEmpty + +[] +let length (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Length + +// ---- Access ---- + +[] +let exactlyOne (array : FlatList<'T>) = Array.exactlyOne (unwrapArray (nameof array) array) + +[] +let head (array : FlatList<'T>) = Array.head (unwrapArray (nameof array) array) + +[] +let item index (array : FlatList<'T>) = Array.item index (unwrapArray (nameof array) array) + +[] +let last (array : FlatList<'T>) = Array.last (unwrapArray (nameof array) array) + +[] +let tail (array : FlatList<'T>) = Array.tail (unwrapArray (nameof array) array) |> wrapArray + +[] +let tryExactlyOne (array : FlatList<'T>) = + checkNotDefault (nameof array) array + if array.Length = 1 then ValueSome array[0] else ValueNone + +[] +let tryHead (array : FlatList<'T>) = + checkNotDefault (nameof array) array + if array.IsEmpty then ValueNone else ValueSome array[0] + +[] +let tryItem index (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if index < 0 || index >= array.Length then + ValueNone + else + ValueSome array[index] + +[] +let tryLast (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if array.IsEmpty then + ValueNone + else + ValueSome array[array.Length - 1] + +[] +let tryTail (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + + if source.Length = 0 then + ValueNone + else + ValueSome (Array.tail source |> wrapArray) + +// ---- Search ---- + +// Array.BinarySearch calls the count of a range length. The functions of this module report it as count, the name of +// their own parameter, with the type and the message of the BCL; the check of a range beyond the end stays with the BCL +let private checkBinarySearchRange index count = + if index < 0 then + raise (ArgumentOutOfRangeException (nameof index, ErrorStrings.NeedNonNegativeNumber)) + + if count < 0 then + raise (ArgumentOutOfRangeException (nameof count, ErrorStrings.NeedNonNegativeNumber)) + +[] +let binarySearch (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + ImmutableArray.BinarySearch (array, value, DefaultComparer<'T>.Instance) + +[] +let binarySearchRange index count (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + checkBinarySearchRange index count + ImmutableArray.BinarySearch (array, index, count, value, DefaultComparer<'T>.Instance) + +[] +let binarySearchRangeWithComparer (comparer : IComparer<'T>) index count (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + checkBinarySearchRange index count + ImmutableArray.BinarySearch (array, index, count, value, comparer) + +[] +let binarySearchWithComparer (comparer : IComparer<'T>) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + ImmutableArray.BinarySearch (array, value, comparer) + +[] +let contains (value : 'T) (array : FlatList<'T>) = Array.contains value (unwrapArray (nameof array) array) + +[] +let exists predicate (array : FlatList<'T>) = Array.exists predicate (unwrapArray (nameof array) array) + +[] +let exists2 predicate (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.exists2 predicate (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let find predicate (array : FlatList<'T>) = Array.find predicate (unwrapArray (nameof array) array) + +[] +let findBack predicate (array : FlatList<'T>) = Array.findBack predicate (unwrapArray (nameof array) array) + +[] +let findIndex predicate (array : FlatList<'T>) = Array.findIndex predicate (unwrapArray (nameof array) array) + +[] +let findIndexBack predicate (array : FlatList<'T>) = Array.findIndexBack predicate (unwrapArray (nameof array) array) + +[] +let forall predicate (array : FlatList<'T>) = Array.forall predicate (unwrapArray (nameof array) array) + +[] +let forall2 predicate (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.forall2 predicate (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let indexOf (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.IndexOf value + +[] +let indexOfRange (startIndex : int) (count : int) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.IndexOf (value, startIndex, count) + +[] +let indexOfRangeWith (comparer : IEqualityComparer<'T>) (startIndex : int) (count : int) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.IndexOf (value, startIndex, count, comparer) + +[] +let indexOfWith (comparer : IEqualityComparer<'T>) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.IndexOf (value, 0, array.Length, comparer) + +[] +let lastIndexOf (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.LastIndexOf value + +[] +let lastIndexOfRange (startIndex : int) (count : int) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.LastIndexOf (value, startIndex, count) + +[] +let lastIndexOfRangeWith (comparer : IEqualityComparer<'T>) (startIndex : int) (count : int) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.LastIndexOf (value, startIndex, count, comparer) + +[] +let lastIndexOfWith (comparer : IEqualityComparer<'T>) (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + // The BCL rejects the start index -1 of an empty array, so an empty array answers without asking it + if array.IsEmpty then + -1 + else + array.LastIndexOf (value, array.Length - 1, array.Length, comparer) + +[] +let pick (chooser : 'T -> 'U voption) (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = 0 + + while result.IsNone && index < source.Length do + result <- chooser source[index] + index <- index + 1 + + match result with + | ValueSome value -> value + | ValueNone -> Raise.indexNotFound () + +[] +let tryFind predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = 0 + + while result.IsNone && index < source.Length do + if predicate source[index] then + result <- ValueSome source[index] + + index <- index + 1 + + result + +[] +let tryFindBack predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = source.Length - 1 + + while result.IsNone && index >= 0 do + if predicate source[index] then + result <- ValueSome source[index] + + index <- index - 1 + + result + +[] +let tryFindIndex predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = 0 + + while result.IsNone && index < source.Length do + if predicate source[index] then + result <- ValueSome index + + index <- index + 1 + + result + +[] +let tryFindIndexBack predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = source.Length - 1 + + while result.IsNone && index >= 0 do + if predicate source[index] then + result <- ValueSome index + + index <- index - 1 + + result + +[] +let tryPick (chooser : 'T -> 'U voption) (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable result = ValueNone + let mutable index = 0 + + while result.IsNone && index < source.Length do + result <- chooser source[index] + index <- index + 1 + + result + +// ---- Transformation ---- + +[] +let add (value : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Add value + +[] +let allPairs (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Arrays.allPairs structPair (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + |> wrapArray + +[] +let append (array1 : FlatList<'T>) (array2 : FlatList<'T>) = + checkNotDefault (nameof array1) array1 + checkNotDefault (nameof array2) array2 + array1.AddRange array2 + +[] +let choose (chooser : 'T -> 'U voption) (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let builder = ImmutableArray.CreateBuilder<'U>() + + for item in source do + match chooser item with + | ValueSome value -> builder.Add value + | ValueNone -> () + + builder.DrainToImmutable () + +[] +let chunkBySize chunkSize (array : FlatList<'T>) = + Array.chunkBySize chunkSize (unwrapArray (nameof array) array) + |> Array.map wrapArray + |> wrapArray + +[] +let collect (mapping : 'T -> FlatList<'U>) (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let builder = ImmutableArray.CreateBuilder<'U>() + + for item in source do + let items = mapping item + // Array.collect dereferences a null result; a default result is a default FlatList, rejected as in concat + checkNotDefault (nameof mapping) items + builder.AddRange items + + builder.DrainToImmutable () + +[] +let concat (arrays : seq>) = + checkNotNullOrDefaultSeq (nameof arrays) arrays + + arrays + |> Seq.map (fun array -> unwrapArray (nameof arrays) array) + |> Array.concat + |> wrapArray + +[] +let filter predicate (array : FlatList<'T>) = + Array.filter predicate (unwrapArray (nameof array) array) + |> wrapArray + +[] +let indexed (array : FlatList<'T>) = + Arrays.indexed structPair (unwrapArray (nameof array) array) + |> wrapArray + +[] +let insertAt index (value : 'T) (source : FlatList<'T>) = + Array.insertAt index value (unwrapArray (nameof source) source) + |> wrapArray + +[] +let insertManyAt index (values : seq<'T>) (source : FlatList<'T>) = + let array = unwrapArray (nameof source) source + + // Array.insertManyAt checks the index before it enumerates the values, and it names the values source then, as + // Seq.toArray does; the check of the values comes after the one of the index and names the parameter of this function + if index >= 0 && index <= array.Length then + checkNotNullOrDefaultSeq (nameof values) values + + Array.insertManyAt index values array |> wrapArray + +[] +let map (mapping : 'T -> 'U) (array : FlatList<'T>) = + Array.map mapping (unwrapArray (nameof array) array) + |> wrapArray + +[] +let map2 (mapping : 'T1 -> 'T2 -> 'U) (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.map2 mapping (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + |> wrapArray + +[] +let map3 (mapping : 'T1 -> 'T2 -> 'T3 -> 'U) (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) (array3 : FlatList<'T3>) = + Array.map3 + mapping + (unwrapArray (nameof array1) array1) + (unwrapArray (nameof array2) array2) + (unwrapArray (nameof array3) array3) + |> wrapArray + +[] +let mapi (mapping : int -> 'T -> 'U) (array : FlatList<'T>) = + Array.mapi mapping (unwrapArray (nameof array) array) + |> wrapArray + +[] +let mapi2 (mapping : int -> 'T1 -> 'T2 -> 'U) (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.mapi2 mapping (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + |> wrapArray + +[] +let pairwise (array : FlatList<'T>) = + Arrays.pairwise structPair (unwrapArray (nameof array) array) + |> wrapArray + +[] +let permute indexMap (array : FlatList<'T>) = + Array.permute indexMap (unwrapArray (nameof array) array) + |> wrapArray + +[] +let remove (item : 'T) (source : FlatList<'T>) = + checkNotDefault (nameof source) source + source.Remove item + +[] +let removeAt index (source : FlatList<'T>) = + Array.removeAt index (unwrapArray (nameof source) source) + |> wrapArray + +[] +let removeMany (items : seq<'T>) (source : FlatList<'T>) = + checkNotDefault (nameof source) source + checkNotNullOrDefaultSeq (nameof items) items + source.RemoveRange items + +[] +let removeManyAt index count (source : FlatList<'T>) = + Array.removeManyAt index count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let removeManyWith (comparer : IEqualityComparer<'T>) (items : seq<'T>) (source : FlatList<'T>) = + checkNotDefault (nameof source) source + checkNotNullOrDefaultSeq (nameof items) items + source.RemoveRange (items, comparer) + +[] +let removeWith (comparer : IEqualityComparer<'T>) (item : 'T) (source : FlatList<'T>) = + checkNotDefault (nameof source) source + source.Remove (item, comparer) + +[] +let replace (oldValue : 'T) (newValue : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Replace (oldValue, newValue) + +[] +let replaceWith (comparer : IEqualityComparer<'T>) (oldValue : 'T) (newValue : 'T) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Replace (oldValue, newValue, comparer) + +[] +let rev (array : FlatList<'T>) = Array.rev (unwrapArray (nameof array) array) |> wrapArray + +[] +let transpose (arrays : seq>) = + checkNotNullOrDefaultSeq (nameof arrays) arrays + + arrays + |> Seq.map (fun array -> unwrapArray (nameof arrays) array) + |> Array.transpose + |> Array.map wrapArray + |> wrapArray + +[] +let unzip (array : FlatList) = + let struct (first, second) = Arrays.unzip id (unwrapArray (nameof array) array) + struct (wrapArray first, wrapArray second) + +[] +let unzip3 (array : FlatList) = + let struct (first, second, third) = Arrays.unzip3 id (unwrapArray (nameof array) array) + struct (wrapArray first, wrapArray second, wrapArray third) + +[] +let updateAt index (value : 'T) (source : FlatList<'T>) = + Array.updateAt index value (unwrapArray (nameof source) source) + |> wrapArray + +[] +let where predicate (array : FlatList<'T>) = + Array.filter predicate (unwrapArray (nameof array) array) + |> wrapArray + +[] +let windowed windowSize (array : FlatList<'T>) = + Array.windowed windowSize (unwrapArray (nameof array) array) + |> Array.map wrapArray + |> wrapArray + +[] +let zip (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + ArrayChecks.sameLengths (nameof array1) source1 (nameof array2) source2 + Arrays.zip structPair source1 source2 |> wrapArray + +[] +let zip3 (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) (array3 : FlatList<'T3>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + let source3 = unwrapArray (nameof array3) array3 + ArrayChecks.sameLengths3 (nameof array1) source1 (nameof array2) source2 (nameof array3) source3 + + Arrays.zip3 structTriple source1 source2 source3 + |> wrapArray + +// ---- Folding ---- + +// The inline functions can use public members only, so they check and unwrap the array themselves +[] +let inline average (array : FlatList< ^T >) : ^T = + if array.IsDefault then + nullArg (nameof array) + + Array.average ( + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + ) + +[] +let inline averageBy ([] projection : 'T -> ^U) (array : FlatList<'T>) : ^U = + if array.IsDefault then + nullArg (nameof array) + + Array.averageBy + projection + (ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull) + +[] +let compareWith comparer (array1 : FlatList<'T>) (array2 : FlatList<'T>) = + Array.compareWith comparer (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let fold (folder : 'State -> 'T -> 'State) (state : 'State) (array : FlatList<'T>) = + Array.fold folder state (unwrapArray (nameof array) array) + +[] +let fold2 (folder : 'State -> 'T1 -> 'T2 -> 'State) (state : 'State) (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.fold2 folder state (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let foldBack (folder : 'T -> 'State -> 'State) (array : FlatList<'T>) (state : 'State) = + Array.foldBack folder (unwrapArray (nameof array) array) state + +[] +let foldBack2 (folder : 'T1 -> 'T2 -> 'State -> 'State) (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) (state : 'State) = + Array.foldBack2 folder (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) state + +[] +let iter action (array : FlatList<'T>) = Array.iter action (unwrapArray (nameof array) array) + +[] +let iter2 action (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.iter2 action (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let iteri action (array : FlatList<'T>) = Array.iteri action (unwrapArray (nameof array) array) + +[] +let iteri2 action (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Array.iteri2 action (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + +[] +let mapFold (mapping : 'State -> 'T -> struct ('Result * 'State)) (state : 'State) (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let results = Array.zeroCreate source.Length + let mutable state = state + + for index = 0 to source.Length - 1 do + let struct (result, next) = mapping state source[index] + results[index] <- result + state <- next + + struct (wrapArray results, state) + +[] +let mapFoldBack (mapping : 'T -> 'State -> struct ('Result * 'State)) (array : FlatList<'T>) (state : 'State) = + let source = unwrapArray (nameof array) array + let results = Array.zeroCreate source.Length + let mutable state = state + + for index = source.Length - 1 downto 0 do + let struct (result, next) = mapping source[index] state + results[index] <- result + state <- next + + struct (wrapArray results, state) + +[] +let max (array : FlatList<'T>) = Array.max (unwrapArray (nameof array) array) + +[] +let maxBy (projection : 'T -> 'U) (array : FlatList<'T>) = Array.maxBy projection (unwrapArray (nameof array) array) + +[] +let min (array : FlatList<'T>) = Array.min (unwrapArray (nameof array) array) + +[] +let minBy (projection : 'T -> 'U) (array : FlatList<'T>) = Array.minBy projection (unwrapArray (nameof array) array) + +[] +let reduce reduction (array : FlatList<'T>) = Array.reduce reduction (unwrapArray (nameof array) array) + +[] +let reduceBack reduction (array : FlatList<'T>) = Array.reduceBack reduction (unwrapArray (nameof array) array) + +[] +let scan (folder : 'State -> 'T -> 'State) (state : 'State) (array : FlatList<'T>) = + Array.scan folder state (unwrapArray (nameof array) array) + |> wrapArray + +[] +let scanBack (folder : 'T -> 'State -> 'State) (array : FlatList<'T>) (state : 'State) = + Array.scanBack folder (unwrapArray (nameof array) array) state + |> wrapArray + +[] +let inline sum (array : FlatList< ^T >) : ^T = + if array.IsDefault then + nullArg (nameof array) + + Array.sum ( + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + ) + +[] +let inline sumBy ([] projection : 'T -> ^U) (array : FlatList<'T>) : ^U = + if array.IsDefault then + nullArg (nameof array) + + Array.sumBy + projection + (ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull) + +// ---- Partitioning and grouping ---- + +[] +let countBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Arrays.countBy structPair projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let distinct (array : FlatList<'T>) = + Array.distinct (unwrapArray (nameof array) array) + |> wrapArray + +[] +let distinctBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Array.distinctBy projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let except (itemsToExclude : seq<'T>) (array : FlatList<'T>) = + // FSharp.Core checks the items to exclude first + checkNotNullOrDefaultSeq (nameof itemsToExclude) itemsToExclude + Array.except itemsToExclude (unwrapArray (nameof array) array) + |> wrapArray + +[] +let groupBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Arrays.groupBy (fun key items -> struct (key, wrapArray items)) projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let partition predicate (array : FlatList<'T>) = + let accepted, rejected = Array.partition predicate (unwrapArray (nameof array) array) + struct (wrapArray accepted, wrapArray rejected) + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (array : FlatList<'T>) = + let first, second = Array.partitionWith partitioner (unwrapArray (nameof array) array) + struct (wrapArray first, wrapArray second) + +// The range functions repeat the checks of Array.skip, Array.take and Array.splitAt and then slice: a slice copies the +// selected elements, unless the range is empty or covers the whole array (ADR 0001, D5) + +[] +let skip count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if count > array.Length then + Raise.invalidArgOutOfRange (nameof count) count "array.Length" array.Length + + if count = array.Length then + empty + else + let count = Operators.max count 0 + array.Slice (count, array.Length - count) + +[] +let skipLast count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if count > array.Length then + Raise.invalidArgOutOfRange (nameof count) count "array.Length" array.Length + + if count = array.Length then + empty + else + array.Slice (0, array.Length - Operators.max count 0) + +[] +let skipWhile predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable index = 0 + + while index < source.Length && predicate source[index] do + index <- index + 1 + + array.Slice (index, source.Length - index) + +[] +let splitAt index (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if index < 0 then + Raise.invalidArgInputMustBeNonNegative (nameof index) index + + if array.Length < index then + Raise.notEnoughElements () + + struct (array.Slice (0, index), array.Slice (index, array.Length - index)) + +[] +let splitInto count (array : FlatList<'T>) = + Array.splitInto count (unwrapArray (nameof array) array) + |> Array.map wrapArray + |> wrapArray + +[] +let sub (array : FlatList<'T>) startIndex count = + checkNotDefault (nameof array) array + + if startIndex < 0 then + Raise.invalidArgInputMustBeNonNegative (nameof startIndex) startIndex + + if count < 0 then + Raise.invalidArgInputMustBeNonNegative (nameof count) count + + // Array.sub compares the sum of the start index and the count with the length, which can overflow; the difference + // cannot, because the start index is not negative + if count > array.Length - startIndex then + Raise.invalidArgOutOfRange (nameof count) count "array.Length" array.Length + + array.Slice (startIndex, count) + +[] +let take count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if count < 0 then + Raise.invalidArgInputMustBeNonNegative (nameof count) count + + if count = 0 then + empty + else + if count > array.Length then + Raise.notEnoughElements () + + array.Slice (0, count) + +[] +let takeLast count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if count < 0 then + Raise.invalidArgInputMustBeNonNegative (nameof count) count + + if count = 0 then + empty + else + if count > array.Length then + Raise.notEnoughElements () + + array.Slice (array.Length - count, count) + +[] +let takeWhile predicate (array : FlatList<'T>) = + let source = unwrapArray (nameof array) array + let mutable count = 0 + + while count < source.Length && predicate source[count] do + count <- count + 1 + + array.Slice (0, count) + +[] +let truncate count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + + if count <= 0 then + empty + else + array.Slice (0, Operators.min count array.Length) + +// ---- Sorting ---- + +[] +let sort (array : FlatList<'T>) = Array.sort (unwrapArray (nameof array) array) |> wrapArray + +[] +let sortBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Array.sortBy projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let sortByDescending (projection : 'T -> 'Key) (array : FlatList<'T>) = + Array.sortByDescending projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let sortDescending (array : FlatList<'T>) = + Array.sortDescending (unwrapArray (nameof array) array) + |> wrapArray + +[] +let sortRange index count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Sort (index, count, DefaultComparer<'T>.Instance) + +[] +let sortRangeWith (comparison : 'T -> 'T -> int) index count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Sort (index, count, Comparer<'T>.Create(Comparison<'T> comparison)) + +[] +let sortRangeWithComparer (comparer : IComparer<'T>) index count (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Sort (index, count, comparer) + +[] +let sortWith comparer (array : FlatList<'T>) = + Array.sortWith comparer (unwrapArray (nameof array) array) + |> wrapArray + +[] +let sortWithComparer (comparer : IComparer<'T>) (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.Sort comparer + +// ---- Random ---- + +[] +let randomChoice (source : FlatList<'T>) = Array.randomChoice (unwrapArray (nameof source) source) + +[] +let randomChoiceBy randomizer (source : FlatList<'T>) = Array.randomChoiceBy randomizer (unwrapArray (nameof source) source) + +[] +let randomChoiceWith (random : Random) (source : FlatList<'T>) = + Array.randomChoiceWith random (unwrapArray (nameof source) source) + +[] +let randomChoices count (source : FlatList<'T>) = + Array.randomChoices count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomChoicesBy randomizer count (source : FlatList<'T>) = + Array.randomChoicesBy randomizer count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomChoicesWith (random : Random) count (source : FlatList<'T>) = + Array.randomChoicesWith random count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomSample count (source : FlatList<'T>) = + Array.randomSample count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomSampleBy randomizer count (source : FlatList<'T>) = + Array.randomSampleBy randomizer count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomSampleWith (random : Random) count (source : FlatList<'T>) = + Array.randomSampleWith random count (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomShuffle (source : FlatList<'T>) = + Array.randomShuffle (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomShuffleBy randomizer (source : FlatList<'T>) = + Array.randomShuffleBy randomizer (unwrapArray (nameof source) source) + |> wrapArray + +[] +let randomShuffleWith (random : Random) (source : FlatList<'T>) = + Array.randomShuffleWith random (unwrapArray (nameof source) source) + |> wrapArray + +// ---- Conversion ---- + +[] +let asMemory (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.AsMemory () + +[] +let asSpan (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array.AsSpan () + +[] +let blit (source : FlatList<'T>) sourceIndex (target : 'T array) targetIndex count = + Array.blit (unwrapArray (nameof source) source) sourceIndex target targetIndex count + +[] +let toArray (array : FlatList<'T>) = Array.copy (unwrapArray (nameof array) array) + +[] +let toList (array : FlatList<'T>) = Array.toList (unwrapArray (nameof array) array) + +[] +let toSeq (array : FlatList<'T>) = + checkNotDefault (nameof array) array + array :> seq<'T> + +// ---- Type-specific ---- + +[] +let isDefault (array : FlatList<'T>) = array.IsDefault + +[] +let isDefaultOrEmpty (array : FlatList<'T>) = array.IsDefaultOrEmpty diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi new file mode 100644 index 00000000..72a7904f --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatList.fsi @@ -0,0 +1,2075 @@ +/// +/// Functions for , the immutable array. +/// +/// +/// +/// The module mirrors : each function has the semantics, the +/// argument checks and the exceptions of the function with the same name in that module (ADR 0001). Functions that +/// FSharp.Core lacks wrap a member of and throw what that +/// member throws. +/// +/// +/// A default immutable array has no underlying array. Every function except +/// +/// and +/// throws for it, as +/// does for a array. +/// +/// +/// Functions that return an optional value return a , and functions +/// that return tuples return struct tuples. The namespaces and +/// contain a module with the same name whose functions use options +/// and reference tuples instead. +/// +/// +[] +module FSharp.Collections.Immutable.FlatList + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// ---- Creation ---- + +/// Creates a flat list whose elements are all the given value. +/// The length of the flat list. +/// The value of every element. +/// The created flat list. +/// Thrown when is negative. +/// O(n). +[] +val create : count : int -> value : 'T -> FlatList<'T> + +/// +/// The empty flat list. +/// +/// +/// O(1). The value is . +/// +[] +[] +val empty<'T> : FlatList<'T> + +/// Creates a flat list from a function of the index. +/// The length of the flat list. +/// The function that computes the element at each index, called in index order. +/// The created flat list. +/// Thrown when is negative. +/// O(n). +[] +val init : count : int -> initializer : (int -> 'T) -> FlatList<'T> + +/// +/// Creates a flat list from a copy of an array. +/// +/// The input array. +/// The flat list of the elements of the array. +/// Thrown when is . +/// +/// O(n). Later changes to the array do not change the flat list. +/// +[] +val ofArray : array : 'T array -> FlatList<'T> + +/// Creates a flat list from a list. +/// The input list. +/// The flat list of the elements of the list. +/// O(n). +[] +val ofList : list : 'T list -> FlatList<'T> + +/// +/// Creates a flat list from a copy of a read-only span. +/// +/// The input span. +/// The flat list of the elements of the span. +/// +/// O(n). The function wraps . +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan<'T> -> FlatList<'T> + +/// Creates a flat list from a sequence. +/// The input sequence. +/// The flat list of the elements of the sequence. +/// Thrown when is or a default flat list. +/// O(n). A flat list is returned as it is, without a copy. +[] +val ofSeq : source : seq<'T> -> FlatList<'T> + +/// +/// Creates a flat list from a copy of a span. +/// +/// The input span. +/// The flat list of the elements of the span. +/// +/// O(n). The function wraps . +/// +[] +val ofSpan : span : Span<'T> -> FlatList<'T> + +/// Creates a flat list whose elements are all the given value. +/// The length of the flat list. +/// The value of every element. +/// The created flat list. +/// Thrown when is negative. +/// O(n). +[] +val replicate : count : int -> initial : 'T -> FlatList<'T> + +/// Creates a flat list of one element. +/// The element. +/// The flat list of one element. +/// O(1). +[] +val singleton : value : 'T -> FlatList<'T> + +/// +/// Creates a flat list from a generator, which returns the next element and the next state. +/// +/// The function that returns the next element and state, or ValueNone to stop. +/// The initial state. +/// The generated flat list. +/// +/// O(n). +/// +[] +val unfold : generator : ('State -> struct ('T * 'State) voption) -> state : 'State -> FlatList<'T> + +// ---- Builder ---- + +/// Creates a flat list with a builder that an action fills. +/// The action that adds the elements to the builder. +/// The flat list of the elements that the action added. +/// O(n). +[] +val build : action : (FlatList<'T>.Builder -> unit) -> FlatList<'T> + +/// +/// Creates an empty builder. +/// +/// A builder of capacity 8. +/// +/// O(1). The function wraps . +/// +[] +val builder : unit -> FlatList<'T>.Builder + +/// +/// Creates an empty builder of a given capacity. +/// +/// The initial capacity. +/// The builder. +/// Thrown when is negative. +/// +/// O(capacity). The function wraps . +/// +[] +val builderWith : capacity : int -> FlatList<'T>.Builder + +/// +/// Returns the contents of a full builder as a flat list without a copy and empties the builder. +/// +/// The builder, whose count must equal its capacity. +/// The flat list of the elements of the builder. +/// Thrown when is . +/// Thrown when the count of the builder differs from its capacity. +/// +/// O(1). The function wraps . +/// +[] +val moveFromBuilder : builder : FlatList<'T>.Builder -> FlatList<'T> + +/// +/// Returns a copy of the contents of a builder as a flat list. +/// +/// The builder, which stays unchanged. +/// The flat list of the elements of the builder. +/// Thrown when is . +/// +/// O(n). The function wraps . +/// +[] +val ofBuilder : builder : FlatList<'T>.Builder -> FlatList<'T> + +/// +/// Creates a builder that contains a copy of the elements of a flat list. +/// +/// The input flat list. +/// The builder. +/// +/// O(n). The function wraps . +/// +[] +val toBuilder : array : FlatList<'T> -> FlatList<'T>.Builder + +/// +/// Creates a flat list from a builder over the elements of a flat list, after an action changes the builder. +/// +/// The action that changes the builder. +/// The input flat list, which stays unchanged. +/// The flat list of the elements of the changed builder. +/// +/// O(n). +/// +[] +val update : action : (FlatList<'T>.Builder -> unit) -> array : FlatList<'T> -> FlatList<'T> + +// ---- Properties ---- + +/// Returns whether the flat list has no elements. +/// The input flat list. +/// when the flat list is empty. +/// O(1). +[] +val isEmpty : array : FlatList<'T> -> bool + +/// Returns the number of elements of the flat list. +/// The input flat list. +/// The length of the flat list. +/// O(1). +[] +val length : array : FlatList<'T> -> int + +// ---- Access ---- + +/// Returns the only element of the flat list. +/// The input flat list. +/// The only element. +/// Thrown when the flat list does not have exactly one element. +/// O(1). +[] +val exactlyOne : array : FlatList<'T> -> 'T + +/// Returns the first element of the flat list. +/// The input flat list. +/// The first element. +/// Thrown when the flat list is empty. +/// O(1). +[] +val head : array : FlatList<'T> -> 'T + +/// Returns the element at an index. +/// The index of the element. +/// The input flat list. +/// The element at the index. +/// Thrown when the index is negative or not less than the length. +/// O(1). +[] +val item : index : int -> array : FlatList<'T> -> 'T + +/// Returns the last element of the flat list. +/// The input flat list. +/// The last element. +/// Thrown when the flat list is empty. +/// O(1). +[] +val last : array : FlatList<'T> -> 'T + +/// Returns the flat list without its first element. +/// The input flat list. +/// A copy of the elements after the first one. +/// Thrown when the flat list is empty. +/// O(n). +[] +val tail : array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the only element of the flat list, or ValueNone when it does not have exactly one element. +/// +/// The input flat list. +/// The only element, or ValueNone. +/// +/// O(1). +/// +[] +val tryExactlyOne : array : FlatList<'T> -> 'T voption + +/// +/// Returns the first element of the flat list, or ValueNone when it is empty. +/// +/// The input flat list. +/// The first element, or ValueNone. +/// +/// O(1). +/// +[] +val tryHead : array : FlatList<'T> -> 'T voption + +/// +/// Returns the element at an index, or ValueNone when the index is out of range. +/// +/// The index of the element. +/// The input flat list. +/// The element at the index, or ValueNone. +/// +/// O(1). +/// +[] +val tryItem : index : int -> array : FlatList<'T> -> 'T voption + +/// +/// Returns the last element of the flat list, or ValueNone when it is empty. +/// +/// The input flat list. +/// The last element, or ValueNone. +/// +/// O(1). +/// +[] +val tryLast : array : FlatList<'T> -> 'T voption + +/// +/// Returns the flat list without its first element, or ValueNone when it is empty. +/// +/// The input flat list. +/// A copy of the elements after the first one, or ValueNone. +/// +/// O(n). +/// +[] +val tryTail : array : FlatList<'T> -> FlatList<'T> voption + +// ---- Search ---- + +/// +/// Searches a sorted flat list for a value with the binary search algorithm. +/// +/// The value to search for. +/// The input flat list, sorted in the order of F# generic comparison. +/// +/// The index of the value, or the bitwise complement of the index of the first larger element, or the bitwise +/// complement of the length when no element is larger. +/// +/// +/// O(log n). The function compares with , +/// the order that produces. +/// +[] +val binarySearch : value : 'T -> array : FlatList<'T> -> int when 'T : comparison + +/// +/// Searches a range of a sorted flat list for a value with the binary search algorithm. +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to search for. +/// The input flat list, sorted in the order of F# generic comparison. +/// +/// The index of the value, or the bitwise complement of the index of the first larger element of the range, or the +/// bitwise complement of the index after the range when no element is larger. +/// +/// Thrown when or is negative. +/// Thrown when the range extends beyond the end of the flat list. +/// +/// +/// O(log count). The result is an index of the flat list, not of the range. The function compares as +/// +/// does. +/// +/// +/// The function wraps +/// +/// and throws what it throws. It names the parameter where the BCL names it length. +/// +/// +[] +val binarySearchRange : index : int -> count : int -> value : 'T -> array : FlatList<'T> -> int when 'T : comparison + +/// +/// Searches a range of a flat list that a comparer sorts for a value with the binary search algorithm. +/// +/// +/// The comparer that orders the flat list. stands for +/// . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to search for. +/// The input flat list, sorted in the order of the comparer. +/// +/// The index of the value, or the bitwise complement of the index of the first larger element of the range, or the +/// bitwise complement of the index after the range when no element is larger. +/// +/// Thrown when or is negative. +/// Thrown when the range extends beyond the end of the flat list. +/// Thrown when the comparison fails: the comparer throws, which becomes the inner exception, or the comparer is and the elements are not comparable. +/// +/// +/// O(log count). The result is an index of the flat list, not of the range. +/// +/// +/// The function wraps +/// +/// and throws what it throws. It names the parameter where the BCL names it length. +/// +/// +[] +val binarySearchRangeWithComparer : + comparer : IComparer<'T> -> index : int -> count : int -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Searches a flat list that a comparer sorts for a value with the binary search algorithm. +/// +/// +/// The comparer that orders the flat list. stands for +/// . +/// +/// The value to search for. +/// The input flat list, sorted in the order of the comparer. +/// +/// The index of the value, or the bitwise complement of the index of the first larger element, or the bitwise +/// complement of the length when no element is larger. +/// +/// Thrown when the comparison fails: the comparer throws, which becomes the inner exception, or the comparer is and the elements are not comparable. +/// +/// O(log n). The function wraps +/// . +/// Unlike +/// it does not need F# comparison, so a comparer of a key or of a culture can order the flat list. +/// +[] +val binarySearchWithComparer : comparer : IComparer<'T> -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns whether the flat list contains an element equal to a value, in F# structural equality. +/// +/// The value to find. +/// The input flat list. +/// when an element equals the value. +/// +/// O(n). +/// +[] +val contains : value : 'T -> array : FlatList<'T> -> bool when 'T : equality + +/// Returns whether any element satisfies the predicate. +/// The function to test the elements. +/// The input flat list. +/// when an element satisfies the predicate. +/// O(n). +[] +val exists : predicate : ('T -> bool) -> array : FlatList<'T> -> bool + +/// +/// Returns whether any pair of elements at the same index satisfies the predicate. +/// +/// The function to test the pairs. +/// The first flat list. +/// The second flat list. +/// when a pair satisfies the predicate. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val exists2 : predicate : ('T1 -> 'T2 -> bool) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> bool + +/// Returns the first element that satisfies the predicate. +/// The function to test the elements. +/// The input flat list. +/// The first element that satisfies the predicate. +/// Thrown when no element satisfies the predicate. +/// O(n). +[] +val find : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T + +/// Returns the last element that satisfies the predicate. +/// The function to test the elements. +/// The input flat list. +/// The last element that satisfies the predicate. +/// Thrown when no element satisfies the predicate. +/// O(n). +[] +val findBack : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T + +/// +/// Returns the index of the first element that satisfies the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the first element that satisfies the predicate. +/// Thrown when no element satisfies the predicate. +/// +/// O(n). +/// +[] +val findIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int + +/// +/// Returns the index of the last element that satisfies the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the last element that satisfies the predicate. +/// Thrown when no element satisfies the predicate. +/// +/// O(n). +/// +[] +val findIndexBack : predicate : ('T -> bool) -> array : FlatList<'T> -> int + +/// Returns whether all elements satisfy the predicate. +/// The function to test the elements. +/// The input flat list. +/// when every element satisfies the predicate. +/// O(n). +[] +val forall : predicate : ('T -> bool) -> array : FlatList<'T> -> bool + +/// +/// Returns whether all pairs of elements at the same index satisfy the predicate. +/// +/// The function to test the pairs. +/// The first flat list. +/// The second flat list. +/// when every pair satisfies the predicate. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val forall2 : predicate : ('T1 -> 'T2 -> bool) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> bool + +/// +/// Returns the index of the first element equal to a value, in . +/// +/// The value to find. +/// The input flat list. +/// The index of the first equal element, or -1. +/// +/// O(n). The function wraps . +/// +[] +val indexOf : value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the first element equal to a value in a range, in +/// . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to find. +/// The input flat list. +/// The index in the flat list of the first equal element of the range, or -1. +/// Thrown when or is outside the flat list. +/// +/// +/// O(count). The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the length, and it accepts for an empty flat list only the start index 0 with the count 0, +/// which gives -1. +/// +/// +[] +val indexOfRange : startIndex : int -> count : int -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the first element equal to a value in a range, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to find. +/// The input flat list. +/// The index in the flat list of the first equal element of the range, or -1. +/// Thrown when or is outside the flat list. +/// +/// +/// O(count). The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the length, and it accepts for an empty flat list only the start index 0 with the count 0, +/// which gives -1. +/// +/// +[] +val indexOfRangeWith : + comparer : IEqualityComparer<'T> -> startIndex : int -> count : int -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the first element equal to a value in a given equality comparer. +/// +/// The equality comparer. +/// The value to find. +/// The input flat list. +/// The index of the first equal element, or -1. +/// +/// O(n). The function wraps +/// . +/// +[] +val indexOfWith : comparer : IEqualityComparer<'T> -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the last element equal to a value, in . +/// +/// The value to find. +/// The input flat list. +/// The index of the last equal element, or -1. +/// +/// O(n). The function wraps . +/// +[] +val lastIndexOf : value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the last element equal to a value in a range that is searched backward, in +/// . +/// +/// The index of the last element of the range, where the search starts. +/// The number of elements of the range, counted backward from the start index. +/// The value to find. +/// The input flat list. +/// The index in the flat list of the last equal element of the range, or -1. +/// Thrown when or is outside the flat list. +/// +/// +/// O(count). The range holds the elements from down to +/// startIndex - count + 1. The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the length, and it accepts for an empty flat list only the start index 0 with the count 0, +/// which gives -1. +/// +/// +[] +val lastIndexOfRange : startIndex : int -> count : int -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the last element equal to a value in a range that is searched backward, in a given equality +/// comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The index of the last element of the range, where the search starts. +/// The number of elements of the range, counted backward from the start index. +/// The value to find. +/// The input flat list. +/// The index in the flat list of the last equal element of the range, or -1. +/// Thrown when or is outside the flat list. +/// +/// +/// O(count). The range holds the elements from down to +/// startIndex - count + 1. The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the length, and it accepts for an empty flat list only the start index 0 with the count 0, +/// which gives -1. +/// +/// +[] +val lastIndexOfRangeWith : + comparer : IEqualityComparer<'T> -> startIndex : int -> count : int -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the index of the last element equal to a value in a given equality comparer. +/// +/// The equality comparer. +/// The value to find. +/// The input flat list. +/// The index of the last equal element, or -1. +/// +/// O(n). The function wraps +/// . +/// +[] +val lastIndexOfWith : comparer : IEqualityComparer<'T> -> value : 'T -> array : FlatList<'T> -> int + +/// +/// Returns the first value that the chooser returns for the elements. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input flat list. +/// The first chosen value. +/// Thrown when the chooser returns ValueNone for every element. +/// +/// O(n). +/// +[] +val pick : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> 'U + +/// +/// Returns the first element that satisfies the predicate, or ValueNone. +/// +/// The function to test the elements. +/// The input flat list. +/// The first element that satisfies the predicate, or ValueNone. +/// +/// O(n). +/// +[] +val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T voption + +/// +/// Returns the last element that satisfies the predicate, or ValueNone. +/// +/// The function to test the elements. +/// The input flat list. +/// The last element that satisfies the predicate, or ValueNone. +/// +/// O(n). +/// +[] +val tryFindBack : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T voption + +/// +/// Returns the index of the first element that satisfies the predicate, or ValueNone. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the first element that satisfies the predicate, or ValueNone. +/// +/// O(n). +/// +[] +val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int voption + +/// +/// Returns the index of the last element that satisfies the predicate, or ValueNone. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the last element that satisfies the predicate, or ValueNone. +/// +/// O(n). +/// +[] +val tryFindIndexBack : predicate : ('T -> bool) -> array : FlatList<'T> -> int voption + +/// +/// Returns the first value that the chooser returns for the elements, or ValueNone. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input flat list. +/// The first chosen value, or ValueNone. +/// +/// O(n). +/// +[] +val tryPick : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> 'U voption + +// ---- Transformation ---- + +/// +/// Returns a flat list with a value added after the last element. +/// +/// The value to add. +/// The input flat list. +/// A flat list of the elements and the value. +/// +/// O(n). The function wraps . +/// +[] +val add : value : 'T -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns every element of the first flat list paired with every element of the second one. +/// +/// The first flat list. +/// The second flat list. +/// The pairs, in the order of the first flat list and then of the second one. +/// +/// O(n × m). +/// +[] +val allPairs : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList + +/// +/// Returns a flat list of the elements of the first flat list followed by the elements of the second one. +/// +/// The first flat list. +/// The second flat list. +/// The concatenation. +/// +/// O(n + m). When one of the flat lists is empty, the function returns the other one. +/// +[] +val append : array1 : FlatList<'T> -> array2 : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the values that the chooser returns for the elements. +/// +/// The function that returns a value, or ValueNone to skip the element. +/// The input flat list. +/// The chosen values. +/// +/// O(n). +/// +[] +val choose : chooser : ('T -> 'U voption) -> array : FlatList<'T> -> FlatList<'U> + +/// Divides the flat list into chunks of a given size. +/// The maximum size of a chunk. +/// The input flat list. +/// The chunks; the last one can be smaller. +/// Thrown when is not positive. +/// O(n). The chunks are copies. +[] +val chunkBySize : chunkSize : int -> array : FlatList<'T> -> FlatList> + +/// +/// Concatenates the flat lists that a function returns for the elements. +/// +/// The function that returns a flat list for each element. +/// The input flat list. +/// The concatenation of the returned flat lists. +/// Thrown when the mapping returns a default flat list. +/// +/// O(n + the total length of the returned flat lists). +/// +[] +val collect : mapping : ('T -> FlatList<'U>) -> array : FlatList<'T> -> FlatList<'U> + +/// Concatenates a sequence of flat lists. +/// The flat lists. +/// The concatenation. +/// Thrown when is or a default flat list, or contains a default flat list. +/// O(the total length). +[] +val concat : arrays : seq> -> FlatList<'T> + +/// +/// Returns the elements that satisfy the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The elements that satisfy the predicate, in their order. +/// +/// O(n). To remove the elements that satisfy a predicate, as +/// does, filter with the +/// negated predicate. +/// +[] +val filter : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> + +/// Returns every element with its index. +/// The input flat list. +/// The pairs of index and element. +/// O(n). +[] +val indexed : array : FlatList<'T> -> FlatList + +/// Returns a flat list with a value inserted at an index. +/// The index of the inserted value, from 0 to the length. +/// The value to insert. +/// The input flat list. +/// The flat list with the value. +/// Thrown when the index is negative or greater than the length. +/// O(n). +[] +val insertAt : index : int -> value : 'T -> source : FlatList<'T> -> FlatList<'T> + +/// Returns a flat list with values inserted at an index. +/// The index of the first inserted value, from 0 to the length. +/// The values to insert. +/// The input flat list. +/// The flat list with the values. +/// Thrown when is or a default flat list. +/// Thrown when the index is negative or greater than the length. +/// O(n + m). +[] +val insertManyAt : index : int -> values : seq<'T> -> source : FlatList<'T> -> FlatList<'T> + +/// Returns the results of a function applied to each element. +/// The function to transform the elements. +/// The input flat list. +/// The results, in the order of the elements. +/// O(n). +[] +val map : mapping : ('T -> 'U) -> array : FlatList<'T> -> FlatList<'U> + +/// +/// Returns the results of a function applied to the pairs of elements at the same index. +/// +/// The function to transform the pairs. +/// The first flat list. +/// The second flat list. +/// The results. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val map2 : mapping : ('T1 -> 'T2 -> 'U) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'U> + +/// +/// Returns the results of a function applied to the triples of elements at the same index. +/// +/// The function to transform the triples. +/// The first flat list. +/// The second flat list. +/// The third flat list. +/// The results. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val map3 : + mapping : ('T1 -> 'T2 -> 'T3 -> 'U) -> + array1 : FlatList<'T1> -> + array2 : FlatList<'T2> -> + array3 : FlatList<'T3> -> + FlatList<'U> + +/// +/// Returns the results of a function applied to each element and its index. +/// +/// The function to transform the elements. +/// The input flat list. +/// The results. +/// +/// O(n). +/// +[] +val mapi : mapping : (int -> 'T -> 'U) -> array : FlatList<'T> -> FlatList<'U> + +/// +/// Returns the results of a function applied to the index and the pair of elements at that index. +/// +/// The function to transform the pairs. +/// The first flat list. +/// The second flat list. +/// The results. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val mapi2 : mapping : (int -> 'T1 -> 'T2 -> 'U) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'U> + +/// Returns every element with its successor. +/// The input flat list. +/// The pairs of neighbours; empty for fewer than two elements. +/// O(n). +[] +val pairwise : array : FlatList<'T> -> FlatList + +/// +/// Returns the elements moved to the indices that a function computes. +/// +/// The function that maps the index of each element to its new index. +/// The input flat list. +/// The permuted flat list. +/// Thrown when the function does not compute a permutation. +/// +/// O(n). +/// +[] +val permute : indexMap : (int -> int) -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without the first element that equals a value, in +/// . +/// +/// The value to remove. +/// The input flat list. +/// The flat list without the element; the input itself when no element equals the value. +/// +/// +/// O(n). The function wraps . +/// +/// +/// To remove several values, use +/// ; +/// to remove by index, use +/// +/// or +/// ; +/// to remove the elements that satisfy a predicate, use +/// +/// with the negated predicate. +/// +/// +[] +val remove : item : 'T -> source : FlatList<'T> -> FlatList<'T> + +/// Returns a flat list without the element at an index. +/// The index of the removed element. +/// The input flat list. +/// The flat list without the element. +/// Thrown when the index is out of range. +/// O(n). +[] +val removeAt : index : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without one element for each of the given values, in +/// . +/// +/// The values to remove. +/// The input flat list. +/// The flat list without the elements; the input itself when no element equals any of the values. +/// Thrown when is or a default flat list. +/// +/// O(n * m) for m values. Each value removes the first element that equals it and that no earlier value has removed; a +/// value that equals no remaining element removes nothing. The function wraps +/// . +/// +[] +val removeMany : items : seq<'T> -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns a flat list without a range of elements. +/// +/// The index of the first removed element. +/// The number of removed elements. +/// The input flat list. +/// The flat list without the range. +/// Thrown when the range is out of range. +/// +/// O(n). The function corresponds to +/// . To remove the +/// elements that equal given values, use +/// . +/// +[] +val removeManyAt : index : int -> count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without one element for each of the given values, in a given equality comparer. +/// +/// The equality comparer. +/// The values to remove. +/// The input flat list. +/// The flat list without the elements; the input itself when no element equals any of the values. +/// Thrown when is or a default flat list. +/// +/// O(n * m) for m values. Each value removes the first element that equals it and that no earlier value has removed; a +/// value that equals no remaining element removes nothing. The function wraps +/// . +/// +[] +val removeManyWith : comparer : IEqualityComparer<'T> -> items : seq<'T> -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without the first element that equals a value in a given equality comparer. +/// +/// The equality comparer. +/// The value to remove. +/// The input flat list. +/// The flat list without the element; the input itself when no element equals the value. +/// +/// O(n). The function wraps +/// . +/// +[] +val removeWith : comparer : IEqualityComparer<'T> -> item : 'T -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Replaces the first element that equals a value with another value, in +/// . +/// +/// The value to replace. +/// The new value. +/// The input flat list. +/// The flat list with the new value in the place of the first element that equals the old value. +/// Thrown when no element equals . +/// +/// +/// O(n). The function wraps . It returns a +/// new flat list also when the new value equals the old one. +/// +/// +/// To replace the element at an index, use +/// . +/// +/// +[] +val replace : oldValue : 'T -> newValue : 'T -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Replaces the first element that equals a value with another value, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The value to replace. +/// The new value. +/// The input flat list. +/// The flat list with the new value in the place of the first element that equals the old value. +/// Thrown when no element equals . +/// +/// O(n). The function wraps +/// . +/// It returns a new flat list also when the new value equals the old one. +/// +[] +val replaceWith : comparer : IEqualityComparer<'T> -> oldValue : 'T -> newValue : 'T -> array : FlatList<'T> -> FlatList<'T> + +/// Returns the elements in reverse order. +/// The input flat list. +/// The reversed flat list. +/// O(n). +[] +val rev : array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the transpose of a sequence of flat lists of the same length. +/// +/// The flat lists. +/// A flat list whose element i holds the element i of every input flat list. +/// Thrown when is or a default flat list, or contains a default flat list. +/// Thrown when the flat lists have different lengths. +/// +/// O(the total length). +/// +[] +val transpose : arrays : seq> -> FlatList> + +/// Splits a flat list of pairs into two flat lists. +/// The input flat list. +/// The first components and the second components. +/// O(n). +[] +val unzip : array : FlatList -> struct (FlatList<'T1> * FlatList<'T2>) + +/// Splits a flat list of triples into three flat lists. +/// The input flat list. +/// The first, the second and the third components. +/// O(n). +[] +val unzip3 : array : FlatList -> struct (FlatList<'T1> * FlatList<'T2> * FlatList<'T3>) + +/// +/// Returns a flat list with the element at an index replaced by a value. +/// +/// The index of the replaced element. +/// The new value. +/// The input flat list. +/// The flat list with the new value. +/// Thrown when the index is out of range. +/// +/// O(n). +/// +[] +val updateAt : index : int -> value : 'T -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the elements that satisfy the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The elements that satisfy the predicate, in their order. +/// +/// O(n). A synonym of . +/// +[] +val where : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the windows of a given size that slide over the flat list. +/// +/// The size of a window. +/// The input flat list. +/// The windows; empty when the flat list is shorter than a window. +/// Thrown when is not positive. +/// +/// O(n × windowSize). The windows are copies. +/// +[] +val windowed : windowSize : int -> array : FlatList<'T> -> FlatList> + +/// Combines two flat lists into a flat list of pairs. +/// The first flat list. +/// The second flat list. +/// The pairs of elements at the same index. +/// Thrown when the flat lists have different lengths. +/// O(n). +[] +val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList + +/// Combines three flat lists into a flat list of triples. +/// The first flat list. +/// The second flat list. +/// The third flat list. +/// The triples of elements at the same index. +/// Thrown when the flat lists have different lengths. +/// O(n). +[] +val zip3 : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> array3 : FlatList<'T3> -> FlatList + +// ---- Folding ---- + +/// Returns the average of the elements. +/// The input flat list. +/// The average. +/// Thrown when the flat list is empty. +/// O(n). +[] +val inline average : + array : FlatList< ^T > -> ^T + when ^T : (static member (+) : ^T * ^T -> ^T) + and ^T : (static member DivideByInt : ^T * int -> ^T) + and ^T : (static member Zero : ^T) + +/// +/// Returns the average of the results of a function applied to each element. +/// +/// The function to transform the elements. +/// The input flat list. +/// The average of the results. +/// Thrown when the flat list is empty. +/// +/// O(n). +/// +[] +val inline averageBy : + projection : ('T -> ^U) -> array : FlatList<'T> -> ^U + when ^U : (static member (+) : ^U * ^U -> ^U) + and ^U : (static member DivideByInt : ^U * int -> ^U) + and ^U : (static member Zero : ^U) + +/// Compares two flat lists element by element with a comparer. +/// The function that compares two elements. +/// The first flat list. +/// The second flat list. +/// +/// The first non-zero result of the comparer, or the comparison of the lengths when every common element compares equal. +/// +/// O(n). +[] +val compareWith : comparer : ('T -> 'T -> int) -> array1 : FlatList<'T> -> array2 : FlatList<'T> -> int + +/// +/// Applies a function to a state and each element, from the first element to the last one. +/// +/// The function that computes the next state. +/// The initial state. +/// The input flat list. +/// The final state. +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'T -> 'State) -> state : 'State -> array : FlatList<'T> -> 'State + +/// +/// Applies a function to a state and each pair of elements at the same index, from the first index to the last one. +/// +/// The function that computes the next state. +/// The initial state. +/// The first flat list. +/// The second flat list. +/// The final state. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val fold2 : + folder : ('State -> 'T1 -> 'T2 -> 'State) -> state : 'State -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> 'State + +/// +/// Applies a function to each element and a state, from the last element to the first one. +/// +/// The function that computes the next state. +/// The input flat list. +/// The initial state. +/// The final state. +/// +/// O(n). +/// +[] +val foldBack : folder : ('T -> 'State -> 'State) -> array : FlatList<'T> -> state : 'State -> 'State + +/// +/// Applies a function to each pair of elements at the same index and a state, from the last index to the first one. +/// +/// The function that computes the next state. +/// The first flat list. +/// The second flat list. +/// The initial state. +/// The final state. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val foldBack2 : + folder : ('T1 -> 'T2 -> 'State -> 'State) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> state : 'State -> 'State + +/// Applies an action to each element. +/// The action. +/// The input flat list. +/// O(n). +[] +val iter : action : ('T -> unit) -> array : FlatList<'T> -> unit + +/// +/// Applies an action to each pair of elements at the same index. +/// +/// The action. +/// The first flat list. +/// The second flat list. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val iter2 : action : ('T1 -> 'T2 -> unit) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> unit + +/// Applies an action to each element and its index. +/// The action. +/// The input flat list. +/// O(n). +[] +val iteri : action : (int -> 'T -> unit) -> array : FlatList<'T> -> unit + +/// +/// Applies an action to the index and the pair of elements at that index. +/// +/// The action. +/// The first flat list. +/// The second flat list. +/// Thrown when the flat lists have different lengths. +/// +/// O(n). +/// +[] +val iteri2 : action : (int -> 'T1 -> 'T2 -> unit) -> array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> unit + +/// +/// Maps each element and threads a state through the mapping, from the first element to the last one. +/// +/// The function that returns the result for an element and the next state. +/// The initial state. +/// The input flat list. +/// The results and the final state. +/// +/// O(n). +/// +[] +val mapFold : + mapping : ('State -> 'T -> struct ('Result * 'State)) -> + state : 'State -> + array : FlatList<'T> -> + struct (FlatList<'Result> * 'State) + +/// +/// Maps each element and threads a state through the mapping, from the last element to the first one. +/// +/// The function that returns the result for an element and the next state. +/// The input flat list. +/// The initial state. +/// The results, in the order of the elements, and the final state. +/// +/// O(n). +/// +[] +val mapFoldBack : + mapping : ('T -> 'State -> struct ('Result * 'State)) -> + array : FlatList<'T> -> + state : 'State -> + struct (FlatList<'Result> * 'State) + +/// Returns the greatest element, in F# generic comparison. +/// The input flat list. +/// The greatest element. +/// Thrown when the flat list is empty. +/// O(n). +[] +val max : array : FlatList<'T> -> 'T when 'T : comparison + +/// +/// Returns the element with the greatest result of a projection. +/// +/// The function to transform the elements for the comparison. +/// The input flat list. +/// The first element with the greatest result. +/// Thrown when the flat list is empty. +/// +/// O(n). +/// +[] +val maxBy : projection : ('T -> 'U) -> array : FlatList<'T> -> 'T when 'U : comparison + +/// Returns the smallest element, in F# generic comparison. +/// The input flat list. +/// The smallest element. +/// Thrown when the flat list is empty. +/// O(n). +[] +val min : array : FlatList<'T> -> 'T when 'T : comparison + +/// +/// Returns the element with the smallest result of a projection. +/// +/// The function to transform the elements for the comparison. +/// The input flat list. +/// The first element with the smallest result. +/// Thrown when the flat list is empty. +/// +/// O(n). +/// +[] +val minBy : projection : ('T -> 'U) -> array : FlatList<'T> -> 'T when 'U : comparison + +/// +/// Combines the elements with a function, from the first element to the last one. +/// +/// The function that combines the result so far with the next element. +/// The input flat list. +/// The combined value. +/// Thrown when the flat list is empty. +/// +/// O(n). +/// +[] +val reduce : reduction : ('T -> 'T -> 'T) -> array : FlatList<'T> -> 'T + +/// +/// Combines the elements with a function, from the last element to the first one. +/// +/// The function that combines an element with the result so far. +/// The input flat list. +/// The combined value. +/// Thrown when the flat list is empty. +/// +/// O(n). +/// +[] +val reduceBack : reduction : ('T -> 'T -> 'T) -> array : FlatList<'T> -> 'T + +/// +/// Returns every state of a fold from the first element to the last one, the initial state first. +/// +/// The function that computes the next state. +/// The initial state. +/// The input flat list. +/// The states; one more than the elements. +/// +/// O(n). +/// +[] +val scan : folder : ('State -> 'T -> 'State) -> state : 'State -> array : FlatList<'T> -> FlatList<'State> + +/// +/// Returns every state of a fold from the last element to the first one, the initial state last. +/// +/// The function that computes the next state. +/// The input flat list. +/// The initial state. +/// The states; one more than the elements. +/// +/// O(n). +/// +[] +val scanBack : folder : ('T -> 'State -> 'State) -> array : FlatList<'T> -> state : 'State -> FlatList<'State> + +/// Returns the sum of the elements. +/// The input flat list. +/// The sum; zero for an empty flat list. +/// O(n). +[] +val inline sum : array : FlatList< ^T > -> ^T when ^T : (static member (+) : ^T * ^T -> ^T) and ^T : (static member Zero : ^T) + +/// +/// Returns the sum of the results of a function applied to each element. +/// +/// The function to transform the elements. +/// The input flat list. +/// The sum of the results; zero for an empty flat list. +/// +/// O(n). +/// +[] +val inline sumBy : + projection : ('T -> ^U) -> array : FlatList<'T> -> ^U + when ^U : (static member (+) : ^U * ^U -> ^U) and ^U : (static member Zero : ^U) + +// ---- Partitioning and grouping ---- + +/// Counts the elements of each key of a projection. +/// The function that returns the key of an element. +/// The input flat list. +/// The keys, in the order of their first occurrence, with the number of their elements. +/// O(n). The keys compare in F# structural equality. +[] +val countBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList when 'Key : equality + +/// +/// Returns the elements without duplicates, in F# structural equality. +/// +/// The input flat list. +/// The first occurrence of each element, in their order. +/// +/// O(n). +/// +[] +val distinct : array : FlatList<'T> -> FlatList<'T> when 'T : equality + +/// +/// Returns the elements without duplicates of the result of a projection. +/// +/// The function that returns the key of an element. +/// The input flat list. +/// The first element of each key, in their order. +/// +/// O(n). +/// +[] +val distinctBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : equality + +/// +/// Returns the distinct elements that do not equal any of the excluded items. +/// +/// The items to remove. +/// The input flat list. +/// The distinct remaining elements, in their order. +/// Thrown when is or a default flat list. +/// +/// O(n + m). +/// +[] +val except : itemsToExclude : seq<'T> -> array : FlatList<'T> -> FlatList<'T> when 'T : equality + +/// Groups the elements by the key of a projection. +/// The function that returns the key of an element. +/// The input flat list. +/// The keys, in the order of their first occurrence, with their elements in their order. +/// O(n). The keys compare in F# structural equality. +[] +val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList)> when 'Key : equality + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input flat list. +/// The elements that satisfy the predicate and the other ones, both in their order. +/// O(n). +[] +val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> struct (FlatList<'T> * FlatList<'T>) + +/// +/// Splits the elements by the choice that a function returns for them. +/// +/// The function that returns Choice1Of2 or Choice2Of2 for an element. +/// The input flat list. +/// The values of the first choices and of the second choices, both in the order of the elements. +/// +/// O(n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> struct (FlatList<'T1> * FlatList<'T2>) + +/// +/// Returns the flat list without its first elements. +/// +/// The number of elements to skip; a negative count skips nothing. +/// The input flat list. +/// The remaining elements. +/// Thrown when the count exceeds the length. +/// +/// O(n). The result is a copy, unless it is empty or the whole flat list. +/// +[] +val skip : count : int -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without its last elements. +/// +/// The number of elements to skip; a negative count skips nothing. +/// The input flat list. +/// The remaining elements. +/// Thrown when the count exceeds the length. +/// +/// O(n). The checks follow . The result is a copy, unless it is empty or the whole flat list. +/// +[] +val skipLast : count : int -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the flat list without its first elements that satisfy the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The elements from the first one that does not satisfy the predicate. +/// +/// O(n). The result is a copy, unless it is empty or the whole flat list. +/// +[] +val skipWhile : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Splits the flat list at an index. +/// +/// The index of the first element of the second part. +/// The input flat list. +/// The elements before the index and the elements from the index. +/// Thrown when the index is negative. +/// Thrown when the index exceeds the length. +/// +/// O(n). The parts are copies, unless they are empty or the whole flat list. +/// +[] +val splitAt : index : int -> array : FlatList<'T> -> struct (FlatList<'T> * FlatList<'T>) + +/// +/// Splits the flat list into at most a given number of parts of nearly equal length. +/// +/// The maximum number of parts. +/// The input flat list. +/// The parts; the first ones are one element longer when the length does not divide evenly. +/// Thrown when is not positive. +/// +/// O(n). The parts are copies. +/// +[] +val splitInto : count : int -> array : FlatList<'T> -> FlatList> + +/// +/// Returns the elements of a range of the flat list. +/// +/// The input flat list. +/// The index of the first element of the range. +/// The number of elements of the range. +/// The elements of the range, in their order. +/// Thrown when or is negative. +/// Thrown when the range extends beyond the end of the flat list. +/// +/// +/// O(count). The result is a copy, unless it is empty or the whole flat list. To view a range without a copy, use +/// or +/// . +/// +/// +/// The function mirrors +/// and wraps . +/// +/// +[] +val sub : array : FlatList<'T> -> startIndex : int -> count : int -> FlatList<'T> + +/// +/// Returns the first elements of the flat list. +/// +/// The number of elements to take. +/// The input flat list. +/// The first elements. +/// Thrown when the count is negative. +/// Thrown when the count exceeds the length. +/// +/// O(n). The result is a copy, unless it is empty or the whole flat list. +/// +[] +val take : count : int -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the last elements of the flat list. +/// +/// The number of elements to take. +/// The input flat list. +/// The last elements. +/// Thrown when the count is negative. +/// Thrown when the count exceeds the length. +/// +/// O(n). The checks follow . The result is a copy, unless it is empty or the whole flat list. +/// +[] +val takeLast : count : int -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the first elements of the flat list that satisfy the predicate. +/// +/// The function to test the elements. +/// The input flat list. +/// The elements before the first one that does not satisfy the predicate. +/// +/// O(n). The result is a copy, unless it is empty or the whole flat list. +/// +[] +val takeWhile : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Returns at most a given number of first elements. +/// +/// The maximum number of elements; a count that is not positive returns an empty flat list. +/// The input flat list. +/// The first elements. +/// +/// O(n). The result is a copy, unless it is empty or the whole flat list. +/// +[] +val truncate : count : int -> array : FlatList<'T> -> FlatList<'T> + +// ---- Sorting ---- + +/// +/// Sorts the elements in the order of F# generic comparison. +/// +/// The input flat list. +/// The sorted flat list. +/// +/// O(n log n). This is not a stable sort, as . +/// +[] +val sort : array : FlatList<'T> -> FlatList<'T> when 'T : comparison + +/// +/// Sorts the elements by the result of a projection, in the order of F# generic comparison. +/// +/// The function that returns the key of an element. +/// The input flat list. +/// The sorted flat list. +/// +/// O(n log n). This is not a stable sort. +/// +[] +val sortBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison + +/// +/// Sorts the elements by the result of a projection, in descending order. +/// +/// The function that returns the key of an element. +/// The input flat list. +/// The sorted flat list. +/// +/// O(n log n). This is not a stable sort. +/// +[] +val sortByDescending : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'T> when 'Key : comparison + +/// +/// Sorts the elements in descending order of F# generic comparison. +/// +/// The input flat list. +/// The sorted flat list. +/// +/// O(n log n). This is not a stable sort. +/// +[] +val sortDescending : array : FlatList<'T> -> FlatList<'T> when 'T : comparison + +/// +/// Sorts a range of the elements in the order of F# generic comparison. +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The input flat list. +/// The flat list with the sorted range; the input itself when the range needs no sorting. +/// Thrown when or is negative, or the range extends beyond the end of the flat list. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// +/// and throws what that member throws for a range out of range. +/// +[] +val sortRange : index : int -> count : int -> array : FlatList<'T> -> FlatList<'T> when 'T : comparison + +/// +/// Sorts a range of the elements with a comparison function. +/// +/// The function that compares two elements. +/// The index of the first element of the range. +/// The number of elements of the range. +/// The input flat list. +/// The flat list with the sorted range; the input itself when the range needs no sorting. +/// Thrown when or is negative, or the range extends beyond the end of the flat list. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// +/// and throws what that member throws for a range out of range. +/// +[] +val sortRangeWith : comparison : ('T -> 'T -> int) -> index : int -> count : int -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Sorts a range of the elements with a comparer. +/// +/// The comparer. +/// The index of the first element of the range. +/// The number of elements of the range. +/// The input flat list. +/// The flat list with the sorted range; the input itself when the range needs no sorting. +/// Thrown when or is negative, or the range extends beyond the end of the flat list. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// +/// and throws what that member throws for a range out of range. +/// +[] +val sortRangeWithComparer : comparer : IComparer<'T> -> index : int -> count : int -> array : FlatList<'T> -> FlatList<'T> + +/// Sorts the elements with a comparison function. +/// The function that compares two elements. +/// The input flat list. +/// The sorted flat list. +/// O(n log n). This is not a stable sort. +[] +val sortWith : comparer : ('T -> 'T -> int) -> array : FlatList<'T> -> FlatList<'T> + +/// +/// Sorts the elements with a comparer. +/// +/// The comparer. +/// The input flat list. +/// The sorted flat list; the input itself when it is already sorted. +/// +/// O(n log n). This is not a stable sort. The function wraps +/// . +/// +[] +val sortWithComparer : comparer : IComparer<'T> -> array : FlatList<'T> -> FlatList<'T> + +// ---- Random ---- + +/// +/// Returns a random element, chosen with . +/// +/// The input flat list. +/// A random element. +/// Thrown when the flat list is empty. +/// +/// O(1). +/// +[] +val randomChoice : source : FlatList<'T> -> 'T + +/// +/// Returns a random element, chosen with a function that returns numbers in [0, 1). +/// +/// The function that returns a random number in [0, 1). +/// The input flat list. +/// A random element. +/// Thrown when the flat list is empty. +/// Thrown when the randomizer returns a number outside [0, 1). +/// +/// O(1). +/// +[] +val randomChoiceBy : randomizer : (unit -> float) -> source : FlatList<'T> -> 'T + +/// +/// Returns a random element, chosen with a given random number generator. +/// +/// The random number generator. +/// The input flat list. +/// A random element. +/// Thrown when is . +/// Thrown when the flat list is empty. +/// +/// O(1). +/// +[] +val randomChoiceWith : random : Random -> source : FlatList<'T> -> 'T + +/// +/// Returns random elements, chosen with ; an element can repeat. +/// +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when the count is negative or the flat list is empty. +/// +/// O(count). +/// +[] +val randomChoices : count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns random elements, chosen with a function that returns numbers in [0, 1); an element can repeat. +/// +/// The function that returns a random number in [0, 1). +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when the count is negative or the flat list is empty. +/// Thrown when the randomizer returns a number outside [0, 1). +/// +/// O(count). +/// +[] +val randomChoicesBy : randomizer : (unit -> float) -> count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns random elements, chosen with a given random number generator; an element can repeat. +/// +/// The random number generator. +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when is . +/// Thrown when the count is negative or the flat list is empty. +/// +/// O(count). +/// +[] +val randomChoicesWith : random : Random -> count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns random elements at distinct indices, chosen with . +/// +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when the count is negative or exceeds the length. +/// +/// O(count). +/// +[] +val randomSample : count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns random elements at distinct indices, chosen with a function that returns numbers in [0, 1). +/// +/// The function that returns a random number in [0, 1). +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when the count is negative or exceeds the length. +/// Thrown when the randomizer returns a number outside [0, 1). +/// +/// O(count). +/// +[] +val randomSampleBy : randomizer : (unit -> float) -> count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns random elements at distinct indices, chosen with a given random number generator. +/// +/// The random number generator. +/// The number of elements to choose. +/// The input flat list. +/// The chosen elements. +/// Thrown when is . +/// Thrown when the count is negative or exceeds the length. +/// +/// O(count). +/// +[] +val randomSampleWith : random : Random -> count : int -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the elements in a random order, chosen with . +/// +/// The input flat list. +/// The shuffled flat list. +/// +/// O(n). +/// +[] +val randomShuffle : source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the elements in a random order, chosen with a function that returns numbers in [0, 1). +/// +/// The function that returns a random number in [0, 1). +/// The input flat list. +/// The shuffled flat list. +/// Thrown when the randomizer returns a number outside [0, 1). +/// +/// O(n). +/// +[] +val randomShuffleBy : randomizer : (unit -> float) -> source : FlatList<'T> -> FlatList<'T> + +/// +/// Returns the elements in a random order, chosen with a given random number generator. +/// +/// The random number generator. +/// The input flat list. +/// The shuffled flat list. +/// Thrown when is . +/// +/// O(n). +/// +[] +val randomShuffleWith : random : Random -> source : FlatList<'T> -> FlatList<'T> + +// ---- Conversion ---- + +/// +/// Returns a read-only memory region over the elements, without a copy. +/// +/// The input flat list. +/// The memory region. +/// +/// O(1). The function wraps . +/// +[] +val asMemory : array : FlatList<'T> -> ReadOnlyMemory<'T> + +/// +/// Returns a read-only span over the elements, without a copy. +/// +/// The input flat list. +/// The span. +/// +/// O(1). The function wraps . A range of the span is a +/// view of a range of the flat list that copies nothing. +/// +[] +val asSpan : array : FlatList<'T> -> ReadOnlySpan<'T> + +/// Copies a range of the flat list into an array. +/// The input flat list. +/// The index of the first element to copy. +/// The array to copy to. +/// The index in the target array of the first copied element. +/// The number of elements to copy. +/// Thrown when is . +/// Thrown when a range is outside its flat list or array. +/// O(count). +[] +val blit : source : FlatList<'T> -> sourceIndex : int -> target : 'T array -> targetIndex : int -> count : int -> unit + +/// Returns a new array with the elements. +/// The input flat list. +/// A copy of the elements. +/// O(n). +[] +val toArray : array : FlatList<'T> -> 'T array + +/// Returns a list of the elements. +/// The input flat list. +/// The list of the elements. +/// O(n). +[] +val toList : array : FlatList<'T> -> 'T list + +/// Returns the flat list as a sequence. +/// The input flat list. +/// The sequence of the elements. +/// O(1). +[] +val toSeq : array : FlatList<'T> -> seq<'T> + +// ---- Type-specific ---- + +/// +/// Returns whether the immutable array is a default value without an underlying array. +/// +/// The input flat list, which can be a default value. +/// for a default value. +/// +/// O(1). The function wraps . +/// +[] +val isDefault : array : FlatList<'T> -> bool + +/// +/// Returns whether the immutable array is a default value or empty. +/// +/// The input flat list, which can be a default value. +/// for a default value or an empty flat list. +/// +/// O(1). The function wraps . +/// +[] +val isDefaultOrEmpty : array : FlatList<'T> -> bool diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fs new file mode 100644 index 00000000..1cf9030b --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fs @@ -0,0 +1,214 @@ +[] +module FSharp.Collections.Immutable.FlatListBuilder + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// ---- Properties ---- + +[] +let capacity (builder : FlatList<'T>.Builder) = builder.Capacity + +[] +let length (builder : FlatList<'T>.Builder) = builder.Count + +// ---- Access ---- + +[] +let item index (builder : FlatList<'T>.Builder) = builder[index] + +// ---- Search ---- + +[] +let contains (value : 'T) (builder : FlatList<'T>.Builder) = builder.Contains value + +[] +let indexOf (value : 'T) (builder : FlatList<'T>.Builder) = builder.IndexOf value + +[] +let indexOfRange (startIndex : int) (count : int) (value : 'T) (builder : FlatList<'T>.Builder) = + builder.IndexOf (value, startIndex, count) + +[] +let indexOfRangeWith + (comparer : IEqualityComparer<'T>) + (startIndex : int) + (count : int) + (value : 'T) + (builder : FlatList<'T>.Builder) + = + builder.IndexOf (value, startIndex, count, comparer) + +[] +let lastIndexOf (value : 'T) (builder : FlatList<'T>.Builder) = builder.LastIndexOf value + +[] +let lastIndexOfRange (startIndex : int) (count : int) (value : 'T) (builder : FlatList<'T>.Builder) = + builder.LastIndexOf (value, startIndex, count) + +[] +let lastIndexOfRangeWith + (comparer : IEqualityComparer<'T>) + (startIndex : int) + (count : int) + (value : 'T) + (builder : FlatList<'T>.Builder) + = + builder.LastIndexOf (value, startIndex, count, comparer) + +// ---- Transformation ---- + +[] +let add (value : 'T) (builder : FlatList<'T>.Builder) = + builder.Add value + builder + +[] +let addRange (values : seq<'T>) (builder : FlatList<'T>.Builder) = + checkNotNullOrDefaultSeq (nameof values) values + builder.AddRange values + builder + +[] +let clear (builder : FlatList<'T>.Builder) = + builder.Clear () + builder + +[] +let insertAt index (value : 'T) (builder : FlatList<'T>.Builder) = + builder.Insert (index, value) + builder + +[] +let insertManyAt index (values : seq<'T>) (builder : FlatList<'T>.Builder) = + // The BCL checks the index before the values, so an index out of range wins over a missing sequence + if index >= 0 && index <= builder.Count then + checkNotNullOrDefaultSeq (nameof values) values + + builder.InsertRange (index, values) + builder + +[] +let remove (value : 'T) (builder : FlatList<'T>.Builder) = builder.Remove value + +[] +let removeAll (predicate : 'T -> bool) (builder : FlatList<'T>.Builder) = + builder.RemoveAll (Predicate<'T> predicate) + builder + +[] +let removeAt index (builder : FlatList<'T>.Builder) = + builder.RemoveAt index + builder + +[] +let removeMany (items : seq<'T>) (builder : FlatList<'T>.Builder) = + checkNotNullOrDefaultSeq (nameof items) items + builder.RemoveRange items + builder + +[] +let removeManyAt index count (builder : FlatList<'T>.Builder) = + // The BCL calls the count length. The checks keep its order, its exception and its message + if index < 0 || index > builder.Count then + raise (ArgumentOutOfRangeException (nameof index)) + + if count < 0 || index > builder.Count - count then + raise (ArgumentOutOfRangeException (nameof count)) + + builder.RemoveRange (index, count) + builder + +[] +let removeManyWith (comparer : IEqualityComparer<'T>) (items : seq<'T>) (builder : FlatList<'T>.Builder) = + checkNotNullOrDefaultSeq (nameof items) items + builder.RemoveRange (items, comparer) + builder + +[] +let removeWith (comparer : IEqualityComparer<'T>) (value : 'T) (builder : FlatList<'T>.Builder) = builder.Remove (value, comparer) + +[] +let replace (oldValue : 'T) (newValue : 'T) (builder : FlatList<'T>.Builder) = + builder.Replace (oldValue, newValue) + builder + +[] +let replaceWith (comparer : IEqualityComparer<'T>) (oldValue : 'T) (newValue : 'T) (builder : FlatList<'T>.Builder) = + builder.Replace (oldValue, newValue, comparer) + builder + +[] +let rev (builder : FlatList<'T>.Builder) = + builder.Reverse () + builder + +[] +let setCapacity capacity (builder : FlatList<'T>.Builder) = + builder.Capacity <- capacity + builder + +[] +let updateAt index (value : 'T) (builder : FlatList<'T>.Builder) = + builder[index] <- value + builder + +// ---- Sorting ---- + +[] +let sort (builder : FlatList<'T>.Builder) = + builder.Sort DefaultComparer<'T>.Instance + builder + +[] +let sortRange index count (builder : FlatList<'T>.Builder) = + builder.Sort (index, count, DefaultComparer<'T>.Instance) + builder + +[] +let sortRangeWith (comparison : 'T -> 'T -> int) index count (builder : FlatList<'T>.Builder) = + builder.Sort (index, count, Comparer<'T>.Create(Comparison<'T> comparison)) + builder + +[] +let sortRangeWithComparer (comparer : IComparer<'T>) index count (builder : FlatList<'T>.Builder) = + builder.Sort (index, count, comparer) + builder + +[] +let sortWith (comparer : 'T -> 'T -> int) (builder : FlatList<'T>.Builder) = + builder.Sort (Comparison<'T> comparer) + builder + +[] +let sortWithComparer (comparer : IComparer<'T>) (builder : FlatList<'T>.Builder) = + builder.Sort comparer + builder + +// ---- Conversion ---- + +[] +let blit (source : FlatList<'T>.Builder) sourceIndex (target : 'T array) targetIndex count = + // The BCL calls the target destination, its index destinationIndex and the count length. The checks keep its + // order, its exceptions and its messages + match withNull target with + | null -> nullArg (nameof target) + | _ -> () + + if count < 0 then + raise (ArgumentOutOfRangeException (nameof count)) + + if sourceIndex < 0 || sourceIndex > source.Count - count then + raise (ArgumentOutOfRangeException (nameof sourceIndex)) + + if targetIndex < 0 || targetIndex > target.Length - count then + raise (ArgumentOutOfRangeException (nameof targetIndex)) + + source.CopyTo (sourceIndex, target, targetIndex, count) + +[] +let drainToFlatList (builder : FlatList<'T>.Builder) : FlatList<'T> = builder.DrainToImmutable () + +[] +let toArray (builder : FlatList<'T>.Builder) = builder.ToArray () diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fsi new file mode 100644 index 00000000..44aeb55e --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListBuilder.fsi @@ -0,0 +1,506 @@ +/// +/// Functions for the builder of a flat list, . +/// +/// +/// The builder is mutable. The functions that change it return the same builder, so that the calls chain with the pipe +/// operator. creates a builder, and +/// +/// returns its contents as a flat list. +/// +[] +module FSharp.Collections.Immutable.FlatListBuilder + +open System.Collections.Generic + +// ---- Properties ---- + +/// +/// Returns the number of elements that the builder can hold without growing. +/// +/// The builder. +/// The capacity. +[] +val capacity : builder : FlatList<'T>.Builder -> int + +/// Returns the number of elements in the builder. +/// The builder. +/// The number of elements. +[] +val length : builder : FlatList<'T>.Builder -> int + +// ---- Access ---- + +/// Returns the element at an index. +/// The index of the element. +/// The builder. +/// The element. +/// Thrown when the index is out of range. +[] +val item : index : int -> builder : FlatList<'T>.Builder -> 'T + +// ---- Search ---- + +/// +/// Returns whether the builder contains a value, in . +/// +/// The value to find. +/// The builder. +/// when an element equals the value. +[] +val contains : value : 'T -> builder : FlatList<'T>.Builder -> bool + +/// +/// Returns the index of the first element equal to a value, or -1. +/// +/// The value to find. +/// The builder. +/// The index, or -1. +[] +val indexOf : value : 'T -> builder : FlatList<'T>.Builder -> int + +/// +/// Returns the index of the first element equal to a value in a range, in +/// . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to find. +/// The builder. +/// The index in the builder of the first equal element of the range, or -1. +/// Thrown when or is outside the builder. +/// +/// +/// O(count). The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the count of the builder, and it accepts for an empty builder only the start index 0 with +/// the count 0, which gives -1. +/// +/// +[] +val indexOfRange : startIndex : int -> count : int -> value : 'T -> builder : FlatList<'T>.Builder -> int + +/// +/// Returns the index of the first element equal to a value in a range, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The value to find. +/// The builder. +/// The index in the builder of the first equal element of the range, or -1. +/// Thrown when or is outside the builder. +/// +/// +/// O(count). The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the count of the builder, and it accepts for an empty builder only the start index 0 with +/// the count 0, which gives -1. +/// +/// +[] +val indexOfRangeWith : + comparer : IEqualityComparer<'T> -> startIndex : int -> count : int -> value : 'T -> builder : FlatList<'T>.Builder -> int + +/// +/// Returns the index of the last element equal to a value, or -1. +/// +/// The value to find. +/// The builder. +/// The index, or -1. +[] +val lastIndexOf : value : 'T -> builder : FlatList<'T>.Builder -> int + +/// +/// Returns the index of the last element equal to a value in a range that is searched backward, in +/// . +/// +/// The index of the last element of the range, where the search starts. +/// The number of elements of the range, counted backward from the start index. +/// The value to find. +/// The builder. +/// The index in the builder of the last equal element of the range, or -1. +/// Thrown when or is outside the builder. +/// +/// +/// O(count). The range holds the elements from down to +/// startIndex - count + 1. The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the count of the builder, and it accepts for an empty builder only the start index 0 with +/// the count 0, which gives -1. +/// +/// +[] +val lastIndexOfRange : startIndex : int -> count : int -> value : 'T -> builder : FlatList<'T>.Builder -> int + +/// +/// Returns the index of the last element equal to a value in a range that is searched backward, in a given equality +/// comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The index of the last element of the range, where the search starts. +/// The number of elements of the range, counted backward from the start index. +/// The value to find. +/// The builder. +/// The index in the builder of the last equal element of the range, or -1. +/// Thrown when or is outside the builder. +/// +/// +/// O(count). The range holds the elements from down to +/// startIndex - count + 1. The function wraps +/// . +/// +/// +/// The function throws what the BCL member throws. The member of .NET 8 and .NET 10 rejects also an empty range at a +/// start index that equals the count of the builder, and it accepts for an empty builder only the start index 0 with +/// the count 0, which gives -1. +/// +/// +[] +val lastIndexOfRangeWith : + comparer : IEqualityComparer<'T> -> startIndex : int -> count : int -> value : 'T -> builder : FlatList<'T>.Builder -> int + +// ---- Transformation ---- + +/// Adds a value after the last element. +/// The value to add. +/// The builder. +/// The same builder. +[] +val add : value : 'T -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Adds values after the last element. +/// The values to add. +/// The builder. +/// The same builder. +/// Thrown when is or a default flat list. +[] +val addRange : values : seq<'T> -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Removes every element. +/// The builder. +/// The same builder. +[] +val clear : builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Inserts a value at an index. +/// The index of the inserted value, from 0 to the count. +/// The value to insert. +/// The builder. +/// The same builder. +/// Thrown when the index is out of range. +[] +val insertAt : index : int -> value : 'T -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Inserts values at an index. +/// +/// The index of the first inserted value, from 0 to the count. +/// The values to insert. +/// The builder. +/// The same builder. +/// Thrown when the index is out of range. +/// Thrown when is or a default flat list. +/// +/// O(n + m) for m values. The function wraps +/// . +/// The index is checked before the values, as the BCL member does. +/// +[] +val insertManyAt : index : int -> values : seq<'T> -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Removes the first element equal to a value, in . +/// +/// The value to remove. +/// The builder. +/// when an element was removed. +[] +val remove : value : 'T -> builder : FlatList<'T>.Builder -> bool + +/// +/// Removes every element that satisfies a predicate. +/// +/// The function to test the elements. +/// The builder. +/// The same builder. +/// +/// O(n). The function wraps +/// . +/// +[] +val removeAll : predicate : ('T -> bool) -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Removes the element at an index. +/// The index of the removed element. +/// The builder. +/// The same builder. +/// Thrown when the index is out of range. +[] +val removeAt : index : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Removes one element for each of the given values, in +/// . +/// +/// The values to remove. +/// The builder. +/// The same builder. +/// Thrown when is or a default flat list. +/// +/// O(n * m) for m values. Each value removes the first remaining element that equals it; a value that equals no remaining +/// element removes nothing. The function wraps +/// . +/// +[] +val removeMany : items : seq<'T> -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Removes a range of elements. +/// +/// The index of the first removed element. +/// The number of removed elements. +/// The builder. +/// The same builder. +/// Thrown when is out of range, or is negative or the range extends beyond the end of the builder. +/// +/// O(n). The function wraps +/// . It +/// names the parameter where the BCL names it length. +/// +[] +val removeManyAt : index : int -> count : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Removes one element for each of the given values, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The values to remove. +/// The builder. +/// The same builder. +/// Thrown when is or a default flat list. +/// +/// O(n * m) for m values. Each value removes the first remaining element that equals it; a value that equals no remaining +/// element removes nothing. The function wraps +/// . +/// +[] +val removeManyWith : comparer : IEqualityComparer<'T> -> items : seq<'T> -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Removes the first element equal to a value, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The value to remove. +/// The builder. +/// when an element was removed. +/// +/// O(n). The function wraps +/// . +/// +[] +val removeWith : comparer : IEqualityComparer<'T> -> value : 'T -> builder : FlatList<'T>.Builder -> bool + +/// +/// Replaces the first element that equals a value with another value, in +/// . +/// +/// The value to replace. +/// The new value. +/// The builder. +/// The same builder. +/// +/// +/// O(n). The function wraps +/// . +/// +/// +/// The builder stays as it is when no element equals the old value: the BCL member does not throw. The function +/// +/// of the flat list throws in that case. +/// +/// +[] +val replace : oldValue : 'T -> newValue : 'T -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Replaces the first element that equals a value with another value, in a given equality comparer. +/// +/// +/// The equality comparer. stands for +/// . +/// +/// The value to replace. +/// The new value. +/// The builder. +/// The same builder. +/// +/// +/// O(n). The function wraps +/// . +/// +/// +/// The builder stays as it is when no element equals the old value: the BCL member does not throw. The function +/// +/// of the flat list throws in that case. +/// +/// +[] +val replaceWith : + comparer : IEqualityComparer<'T> -> oldValue : 'T -> newValue : 'T -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Reverses the order of the elements. +/// The builder. +/// The same builder. +[] +val rev : builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Sets the number of elements that the builder can hold without growing. +/// +/// The new capacity, not less than the count. +/// The builder. +/// The same builder. +/// Thrown when the capacity is less than the count. +[] +val setCapacity : capacity : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Replaces the element at an index. +/// The index of the replaced element. +/// The new value. +/// The builder. +/// The same builder. +/// Thrown when the index is out of range. +[] +val updateAt : index : int -> value : 'T -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +// ---- Sorting ---- + +/// Sorts the elements in the order of F# generic comparison. +/// The builder. +/// The same builder. +/// This is not a stable sort. +[] +val sort : builder : FlatList<'T>.Builder -> FlatList<'T>.Builder when 'T : comparison + +/// +/// Sorts a range of the elements in the order of F# generic comparison. +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The builder. +/// The same builder. +/// Thrown when or is negative, or the range extends beyond the end of the builder. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// . +/// +[] +val sortRange : index : int -> count : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder when 'T : comparison + +/// +/// Sorts a range of the elements with a comparison function. +/// +/// The function that compares two elements. +/// The index of the first element of the range. +/// The number of elements of the range. +/// The builder. +/// The same builder. +/// Thrown when or is negative, or the range extends beyond the end of the builder. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// . +/// +[] +val sortRangeWith : + comparison : ('T -> 'T -> int) -> index : int -> count : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// +/// Sorts a range of the elements with a comparer. +/// +/// +/// The comparer. stands for . +/// +/// The index of the first element of the range. +/// The number of elements of the range. +/// The builder. +/// The same builder. +/// Thrown when or is negative, or the range extends beyond the end of the builder. +/// +/// O(count log count). This is not a stable sort. The elements outside the range keep their places. The function wraps +/// . +/// +[] +val sortRangeWithComparer : + comparer : IComparer<'T> -> index : int -> count : int -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Sorts the elements with a comparison function. +/// The function that compares two elements. +/// The builder. +/// The same builder. +/// This is not a stable sort. +[] +val sortWith : comparer : ('T -> 'T -> int) -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +/// Sorts the elements with a comparer. +/// The comparer. +/// The builder. +/// The same builder. +/// This is not a stable sort. +[] +val sortWithComparer : comparer : IComparer<'T> -> builder : FlatList<'T>.Builder -> FlatList<'T>.Builder + +// ---- Conversion ---- + +/// +/// Copies a range of the builder into an array. +/// +/// The builder. +/// The index of the first element to copy. +/// The array to copy to. +/// The index in the target array of the first copied element. +/// The number of elements to copy. +/// Thrown when is . +/// Thrown when a range is outside its builder or array. +/// +/// O(count). The function wraps +/// . +/// It names the parameters , and where +/// the BCL names them destination, destinationIndex and length. +/// +[] +val blit : source : FlatList<'T>.Builder -> sourceIndex : int -> target : 'T array -> targetIndex : int -> count : int -> unit + +/// +/// Returns the contents of the builder as a flat list and empties the builder. +/// +/// The builder. +/// The flat list of the elements; without a copy when the count equals the capacity. +[] +val drainToFlatList : builder : FlatList<'T>.Builder -> FlatList<'T> + +/// Returns a new array with the elements of the builder. +/// The builder. +/// A copy of the elements. +[] +val toArray : builder : FlatList<'T>.Builder -> 'T array diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fs new file mode 100644 index 00000000..8834ade9 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fs @@ -0,0 +1,76 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Immutable + +type FlatListBuffer<'T> = FlatList<'T>.Builder -> unit + +[] +type FlatListCE () = + + member inline _.Zero () : FlatListBuffer<'T> = ignore + + member inline _.Yield (item : 'T) : FlatListBuffer<'T> = fun builder -> builder.Add item + + member inline _.YieldFrom (items : seq<'T>) : FlatListBuffer<'T> = + // A default flat list given as a sequence is still a default flat list, as in FlatList.ofSeq (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof items) items + fun builder -> builder.AddRange items + + member inline _.YieldFrom (items : 'T array) : FlatListBuffer<'T> = fun builder -> builder.AddRange items + + member inline _.YieldFrom (items : FlatList<'T>) : FlatListBuffer<'T> = + // The builder would dereference the missing array of a default flat list; the expression rejects it as the + // functions of FlatList do (ADR 0001, D6) + checkNotDefault (nameof items) items + fun builder -> builder.AddRange items + + member inline _.YieldFrom (buffer : FlatListBuffer<'T>) : FlatListBuffer<'T> = buffer + + member inline _.Combine (first : FlatListBuffer<'T>, second : FlatListBuffer<'T>) : FlatListBuffer<'T> = + fun builder -> + first builder + second builder + + member inline _.Delay ([] delayed : unit -> FlatListBuffer<'T>) : FlatListBuffer<'T> = + fun builder -> delayed () builder + + member inline _.For (source : seq<'U>, [] body : 'U -> FlatListBuffer<'T>) : FlatListBuffer<'T> = + // A default flat list is a sequence that fails when it is enumerated; the expression rejects it as yield! does + checkNotNullOrDefaultSeq (nameof source) source + + fun builder -> + for item in source do + body item builder + + member inline _.While ([] guard : unit -> bool, body : FlatListBuffer<'T>) : FlatListBuffer<'T> = + fun builder -> + while guard () do + body builder + + member inline _.TryWith + (body : FlatListBuffer<'T>, [] handler : exn -> FlatListBuffer<'T>) + : FlatListBuffer<'T> + = + fun builder -> + try + body builder + with ex -> + handler ex builder + + member inline _.TryFinally (body : FlatListBuffer<'T>, [] compensation : unit -> unit) : FlatListBuffer<'T> = + fun builder -> + try + body builder + finally + compensation () + + member inline _.Using (resource : 'Resource, [] body : 'Resource -> FlatListBuffer<'T>) : FlatListBuffer<'T> = + fun builder -> + use resource = resource + body resource builder + + member _.Run (buffer : FlatListBuffer<'T>) : FlatList<'T> = + let builder = ImmutableArray.CreateBuilder<'T>() + buffer builder + builder.DrainToImmutable () diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fsi new file mode 100644 index 00000000..33c851fa --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListCE.fsi @@ -0,0 +1,72 @@ +namespace FSharp.Collections.Immutable + +open System + +/// +/// The body of a flatList { } expression: a function that adds the elements to a builder. +/// +type FlatListBuffer<'T> = FlatList<'T>.Builder -> unit + +/// +/// The builder of the flatList { } computation expression. +/// +/// +/// The expression collects its elements in an and +/// returns them without a copy when the builder is full (ADR 0001, D7). +/// +[] +type FlatListCE = + + /// Creates the builder of the expression. + new : unit -> FlatListCE + + /// An expression without elements. + member inline Zero : unit -> FlatListBuffer<'T> + + /// Adds an element. + member inline Yield : item : 'T -> FlatListBuffer<'T> + + /// Adds the elements of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline YieldFrom : items : seq<'T> -> FlatListBuffer<'T> + + /// Adds the elements of an array. + /// Thrown when the array is . + member inline YieldFrom : items : 'T array -> FlatListBuffer<'T> + + /// Adds the elements of a flat list. + /// Thrown when the flat list is a default flat list. + member inline YieldFrom : items : FlatList<'T> -> FlatListBuffer<'T> + + /// Adds the elements of another expression body. + member inline YieldFrom : buffer : FlatListBuffer<'T> -> FlatListBuffer<'T> + + /// Adds the elements of two parts of the expression, in order. + member inline Combine : first : FlatListBuffer<'T> * second : FlatListBuffer<'T> -> FlatListBuffer<'T> + + /// + /// Runs the body of the expression each time the elements are collected. + /// + member inline Delay : [] delayed : (unit -> FlatListBuffer<'T>) -> FlatListBuffer<'T> + + /// Adds the elements of the body for each item of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline For : source : seq<'U> * [] body : ('U -> FlatListBuffer<'T>) -> FlatListBuffer<'T> + + /// Adds the elements of the body while the guard holds. + member inline While : [] guard : (unit -> bool) * body : FlatListBuffer<'T> -> FlatListBuffer<'T> + + /// Handles the exceptions of the body. + member inline TryWith : + body : FlatListBuffer<'T> * [] handler : (exn -> FlatListBuffer<'T>) -> FlatListBuffer<'T> + + /// Runs the compensation after the body. + member inline TryFinally : body : FlatListBuffer<'T> * [] compensation : (unit -> unit) -> FlatListBuffer<'T> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> FlatListBuffer<'T>) -> FlatListBuffer<'T> + when 'Resource :> IDisposable | null + + /// Collects the elements of the expression. + member Run : buffer : FlatListBuffer<'T> -> FlatList<'T> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs new file mode 100644 index 00000000..b510cf8b --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fs @@ -0,0 +1,58 @@ +[] +module FSharp.Collections.Immutable.Option.FlatList + +open FSharp.Collections.Immutable + +// The functions call the FSharp.Core Array functions, which take and return options, over the underlying array, or +// convert the result of the default module where it implements the function itself. + +// ---- Creation ---- + +[] +let unfold (generator : 'State -> ('T * 'State) option) (state : 'State) = Array.unfold generator state |> wrapArray + +// ---- Access ---- + +[] +let tryExactlyOne (array : FlatList<'T>) = Array.tryExactlyOne (unwrapArray (nameof array) array) + +[] +let tryHead (array : FlatList<'T>) = Array.tryHead (unwrapArray (nameof array) array) + +[] +let tryItem index (array : FlatList<'T>) = Array.tryItem index (unwrapArray (nameof array) array) + +[] +let tryLast (array : FlatList<'T>) = Array.tryLast (unwrapArray (nameof array) array) + +[] +let tryTail (array : FlatList<'T>) = + FSharp.Collections.Immutable.FlatList.tryTail array + |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'T -> 'U option) (array : FlatList<'T>) = Array.pick chooser (unwrapArray (nameof array) array) + +[] +let tryFind predicate (array : FlatList<'T>) = Array.tryFind predicate (unwrapArray (nameof array) array) + +[] +let tryFindBack predicate (array : FlatList<'T>) = Array.tryFindBack predicate (unwrapArray (nameof array) array) + +[] +let tryFindIndex predicate (array : FlatList<'T>) = Array.tryFindIndex predicate (unwrapArray (nameof array) array) + +[] +let tryFindIndexBack predicate (array : FlatList<'T>) = Array.tryFindIndexBack predicate (unwrapArray (nameof array) array) + +[] +let tryPick (chooser : 'T -> 'U option) (array : FlatList<'T>) = Array.tryPick chooser (unwrapArray (nameof array) array) + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U option) (array : FlatList<'T>) = + Array.choose chooser (unwrapArray (nameof array) array) + |> wrapArray diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi new file mode 100644 index 00000000..33a1a000 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListOption.fsi @@ -0,0 +1,175 @@ +/// +/// The functions of that take or return an optional +/// value, with instead of +/// . +/// +/// +/// After open FSharp.Collections.Immutable.Option, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in where that module has one. +/// +[] +module FSharp.Collections.Immutable.Option.FlatList + +open FSharp.Collections.Immutable + +// ---- Creation ---- + +/// +/// Creates a flat list from a generator, which returns the next element and the next state. +/// +/// The function that returns the next element and state, or None to stop. +/// The initial state. +/// The generated flat list. +/// +/// O(n). +/// +[] +val unfold : generator : ('State -> ('T * 'State) option) -> state : 'State -> FlatList<'T> + +// ---- Access ---- + +/// +/// Returns the only element of the flat list, or None when it does not have exactly one element. +/// +/// The input flat list. +/// The only element, or None. +/// +/// O(1). +/// +[] +val tryExactlyOne : array : FlatList<'T> -> 'T option + +/// +/// Returns the first element of the flat list, or None when it is empty. +/// +/// The input flat list. +/// The first element, or None. +/// +/// O(1). +/// +[] +val tryHead : array : FlatList<'T> -> 'T option + +/// +/// Returns the element at an index, or None when the index is out of range. +/// +/// The index of the element. +/// The input flat list. +/// The element at the index, or None. +/// +/// O(1). +/// +[] +val tryItem : index : int -> array : FlatList<'T> -> 'T option + +/// +/// Returns the last element of the flat list, or None when it is empty. +/// +/// The input flat list. +/// The last element, or None. +/// +/// O(1). +/// +[] +val tryLast : array : FlatList<'T> -> 'T option + +/// +/// Returns the flat list without its first element, or None when it is empty. +/// +/// The input flat list. +/// A copy of the elements after the first one, or None. +/// +/// O(n). +/// +[] +val tryTail : array : FlatList<'T> -> FlatList<'T> option + +// ---- Search ---- + +/// +/// Returns the first value that the chooser returns for the elements. +/// +/// The function that returns a value, or None to go on. +/// The input flat list. +/// The first chosen value. +/// Thrown when the chooser returns None for every element. +/// +/// O(n). +/// +[] +val pick : chooser : ('T -> 'U option) -> array : FlatList<'T> -> 'U + +/// +/// Returns the first element that satisfies the predicate, or None. +/// +/// The function to test the elements. +/// The input flat list. +/// The first element that satisfies the predicate, or None. +/// +/// O(n). +/// +[] +val tryFind : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T option + +/// +/// Returns the last element that satisfies the predicate, or None. +/// +/// The function to test the elements. +/// The input flat list. +/// The last element that satisfies the predicate, or None. +/// +/// O(n). +/// +[] +val tryFindBack : predicate : ('T -> bool) -> array : FlatList<'T> -> 'T option + +/// +/// Returns the index of the first element that satisfies the predicate, or None. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the first element that satisfies the predicate, or None. +/// +/// O(n). +/// +[] +val tryFindIndex : predicate : ('T -> bool) -> array : FlatList<'T> -> int option + +/// +/// Returns the index of the last element that satisfies the predicate, or None. +/// +/// The function to test the elements. +/// The input flat list. +/// The index of the last element that satisfies the predicate, or None. +/// +/// O(n). +/// +[] +val tryFindIndexBack : predicate : ('T -> bool) -> array : FlatList<'T> -> int option + +/// +/// Returns the first value that the chooser returns for the elements, or None. +/// +/// The function that returns a value, or None to go on. +/// The input flat list. +/// The first chosen value, or None. +/// +/// O(n). +/// +[] +val tryPick : chooser : ('T -> 'U option) -> array : FlatList<'T> -> 'U option + +// ---- Transformation ---- + +/// +/// Returns the values that the chooser returns for the elements. +/// +/// The function that returns a value, or None to skip the element. +/// The input flat list. +/// The chosen values. +/// +/// O(n). +/// +[] +val choose : chooser : ('T -> 'U option) -> array : FlatList<'T> -> FlatList<'U> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs new file mode 100644 index 00000000..7425d044 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fs @@ -0,0 +1,112 @@ +[] +module FSharp.Collections.Immutable.Tuple.FlatList + +open FSharp.Collections.Immutable + +// The functions that build a tuple per element share their algorithm with the default module and only pass another +// tuple constructor; the others delegate to the default module and repack the tuples, which costs no second pass. + +// ---- Creation ---- + +[] +let unfold (generator : 'State -> ('T * 'State) voption) (state : 'State) = + let generateStruct state = + match generator state with + | ValueSome (item, next) -> ValueSome (struct (item, next)) + | ValueNone -> ValueNone + + FSharp.Collections.Immutable.FlatList.unfold generateStruct state + +// ---- Transformation ---- + +[] +let allPairs (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + Arrays.allPairs referencePair (unwrapArray (nameof array1) array1) (unwrapArray (nameof array2) array2) + |> wrapArray + +[] +let indexed (array : FlatList<'T>) = + Arrays.indexed referencePair (unwrapArray (nameof array) array) + |> wrapArray + +[] +let pairwise (array : FlatList<'T>) = + Arrays.pairwise referencePair (unwrapArray (nameof array) array) + |> wrapArray + +[] +let unzip (array : FlatList<'T1 * 'T2>) = + let struct (first, second) = + Arrays.unzip (fun (item1, item2) -> struct (item1, item2)) (unwrapArray (nameof array) array) + + wrapArray first, wrapArray second + +[] +let unzip3 (array : FlatList<'T1 * 'T2 * 'T3>) = + let struct (first, second, third) = + Arrays.unzip3 (fun (item1, item2, item3) -> struct (item1, item2, item3)) (unwrapArray (nameof array) array) + + wrapArray first, wrapArray second, wrapArray third + +[] +let zip (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + ArrayChecks.sameLengths (nameof array1) source1 (nameof array2) source2 + Arrays.zip referencePair source1 source2 |> wrapArray + +[] +let zip3 (array1 : FlatList<'T1>) (array2 : FlatList<'T2>) (array3 : FlatList<'T3>) = + let source1 = unwrapArray (nameof array1) array1 + let source2 = unwrapArray (nameof array2) array2 + let source3 = unwrapArray (nameof array3) array3 + ArrayChecks.sameLengths3 (nameof array1) source1 (nameof array2) source2 (nameof array3) source3 + Arrays.zip3 referenceTriple source1 source2 source3 + |> wrapArray + +// ---- Folding ---- + +[] +let mapFold (mapping : 'State -> 'T -> 'Result * 'State) (state : 'State) (array : FlatList<'T>) = + let mapStruct state item = + let result, next = mapping state item + struct (result, next) + + let struct (results, final) = FSharp.Collections.Immutable.FlatList.mapFold mapStruct state array + results, final + +[] +let mapFoldBack (mapping : 'T -> 'State -> 'Result * 'State) (array : FlatList<'T>) (state : 'State) = + let mapStruct item state = + let result, next = mapping item state + struct (result, next) + + let struct (results, final) = FSharp.Collections.Immutable.FlatList.mapFoldBack mapStruct array state + results, final + +// ---- Partitioning and grouping ---- + +[] +let countBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Arrays.countBy referencePair projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let groupBy (projection : 'T -> 'Key) (array : FlatList<'T>) = + Arrays.groupBy (fun key items -> key, wrapArray items) projection (unwrapArray (nameof array) array) + |> wrapArray + +[] +let partition predicate (array : FlatList<'T>) = + let struct (accepted, rejected) = FSharp.Collections.Immutable.FlatList.partition predicate array + accepted, rejected + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (array : FlatList<'T>) = + let struct (first, second) = FSharp.Collections.Immutable.FlatList.partitionWith partitioner array + first, second + +[] +let splitAt index (array : FlatList<'T>) = + let struct (before, after) = FSharp.Collections.Immutable.FlatList.splitAt index array + before, after diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi new file mode 100644 index 00000000..6000cc94 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTuple.fsi @@ -0,0 +1,171 @@ +/// +/// The functions of that take or return tuples, with +/// reference tuples instead of struct tuples. +/// +/// +/// After open FSharp.Collections.Immutable.Tuple, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Optional values stay +/// ; open the namespace +/// after this one for the shapes of +/// . +/// +[] +module FSharp.Collections.Immutable.Tuple.FlatList + +open FSharp.Collections.Immutable + +// ---- Creation ---- + +/// +/// Creates a flat list from a generator, which returns the next element and the next state. +/// +/// The function that returns the next element and state, or ValueNone to stop. +/// The initial state. +/// The generated flat list. +/// +/// O(n). +/// +[] +val unfold : generator : ('State -> ('T * 'State) voption) -> state : 'State -> FlatList<'T> + +// ---- Transformation ---- + +/// +/// Returns every element of the first flat list paired with every element of the second one. +/// +/// The first flat list. +/// The second flat list. +/// The pairs, in the order of the first flat list and then of the second one. +/// +/// O(n × m). +/// +[] +val allPairs : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'T1 * 'T2> + +/// Returns every element with its index. +/// The input flat list. +/// The pairs of index and element. +/// O(n). +[] +val indexed : array : FlatList<'T> -> FlatList + +/// Returns every element with its successor. +/// The input flat list. +/// The pairs of neighbours; empty for fewer than two elements. +/// O(n). +[] +val pairwise : array : FlatList<'T> -> FlatList<'T * 'T> + +/// Splits a flat list of pairs into two flat lists. +/// The input flat list. +/// The first components and the second components. +/// O(n). +[] +val unzip : array : FlatList<'T1 * 'T2> -> FlatList<'T1> * FlatList<'T2> + +/// Splits a flat list of triples into three flat lists. +/// The input flat list. +/// The first, the second and the third components. +/// O(n). +[] +val unzip3 : array : FlatList<'T1 * 'T2 * 'T3> -> FlatList<'T1> * FlatList<'T2> * FlatList<'T3> + +/// Combines two flat lists into a flat list of pairs. +/// The first flat list. +/// The second flat list. +/// The pairs of elements at the same index. +/// Thrown when the flat lists have different lengths. +/// O(n). +[] +val zip : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> FlatList<'T1 * 'T2> + +/// Combines three flat lists into a flat list of triples. +/// The first flat list. +/// The second flat list. +/// The third flat list. +/// The triples of elements at the same index. +/// Thrown when the flat lists have different lengths. +/// O(n). +[] +val zip3 : array1 : FlatList<'T1> -> array2 : FlatList<'T2> -> array3 : FlatList<'T3> -> FlatList<'T1 * 'T2 * 'T3> + +// ---- Folding ---- + +/// +/// Maps each element and threads a state through the mapping, from the first element to the last one. +/// +/// The function that returns the result for an element and the next state. +/// The initial state. +/// The input flat list. +/// The results and the final state. +/// +/// O(n). +/// +[] +val mapFold : mapping : ('State -> 'T -> 'Result * 'State) -> state : 'State -> array : FlatList<'T> -> FlatList<'Result> * 'State + +/// +/// Maps each element and threads a state through the mapping, from the last element to the first one. +/// +/// The function that returns the result for an element and the next state. +/// The input flat list. +/// The initial state. +/// The results, in the order of the elements, and the final state. +/// +/// O(n). +/// +[] +val mapFoldBack : + mapping : ('T -> 'State -> 'Result * 'State) -> array : FlatList<'T> -> state : 'State -> FlatList<'Result> * 'State + +// ---- Partitioning and grouping ---- + +/// Counts the elements of each key of a projection. +/// The function that returns the key of an element. +/// The input flat list. +/// The keys, in the order of their first occurrence, with the number of their elements. +/// O(n). The keys compare in F# structural equality. +[] +val countBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'Key * int> when 'Key : equality + +/// Groups the elements by the key of a projection. +/// The function that returns the key of an element. +/// The input flat list. +/// The keys, in the order of their first occurrence, with their elements in their order. +/// O(n). The keys compare in F# structural equality. +[] +val groupBy : projection : ('T -> 'Key) -> array : FlatList<'T> -> FlatList<'Key * FlatList<'T>> when 'Key : equality + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input flat list. +/// The elements that satisfy the predicate and the other ones, both in their order. +/// O(n). +[] +val partition : predicate : ('T -> bool) -> array : FlatList<'T> -> FlatList<'T> * FlatList<'T> + +/// +/// Splits the elements by the choice that a function returns for them. +/// +/// The function that returns Choice1Of2 or Choice2Of2 for an element. +/// The input flat list. +/// The values of the first choices and of the second choices, both in the order of the elements. +/// +/// O(n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> array : FlatList<'T> -> FlatList<'T1> * FlatList<'T2> + +/// +/// Splits the flat list at an index. +/// +/// The index of the first element of the second part. +/// The input flat list. +/// The elements before the index and the elements from the index. +/// Thrown when the index is negative. +/// Thrown when the index exceeds the length. +/// +/// O(n). The parts are copies, unless they are empty or the whole flat list. +/// +[] +val splitAt : index : int -> array : FlatList<'T> -> FlatList<'T> * FlatList<'T> diff --git a/src/FSharp.Collections.Immutable/FlatList/FlatListTypes.fs b/src/FSharp.Collections.Immutable/FlatList/FlatListTypes.fs new file mode 100644 index 00000000..ae31a470 --- /dev/null +++ b/src/FSharp.Collections.Immutable/FlatList/FlatListTypes.fs @@ -0,0 +1,14 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Immutable + +// The abbreviation has its own file because the module of the folder is a root module, which cannot declare a type of +// the namespace; a file of abbreviations needs no signature file (ADR 0001, D2). + +/// +/// An immutable array, . +/// +/// +/// The name comes from a similar construct in the sources of the F# compiler. +/// +type FlatList<'T> = ImmutableArray<'T> diff --git a/src/FSharp.Collections.Immutable/Helper.fs b/src/FSharp.Collections.Immutable/Helper.fs index b4e365b7..efb31d80 100644 --- a/src/FSharp.Collections.Immutable/Helper.fs +++ b/src/FSharp.Collections.Immutable/Helper.fs @@ -4,7 +4,11 @@ namespace global namespace FSharp.Collections.Immutable #endif +open System +open System.Collections.Generic open System.Collections.Immutable +open System.Linq +open System.Runtime.InteropServices /// Argument checks shared by the collection modules. /// The module keeps its old name, so that the modules written before ADR 0001 compile unchanged until they are replaced. @@ -28,10 +32,423 @@ module internal ImmutableCollectionUtil = if array.IsDefault then nullArg name -/// Exception messages of the modules written before ADR 0001. + /// + /// Tells whether an object is a default , of any element type. + /// + /// + /// + /// is covariant, so a sequence of a reference type is also a + /// sequence of its base types: a default ImmutableArray<string> can be a seq<obj>. The type + /// test :? ImmutableArray<obj> does not recognize it, because the type is another instantiation. + /// + /// + /// The function finds the instantiation from the type of the object and reads the property IsDefault through + /// reflection. It allocates nothing for an object of another type. It is inline, because the public inline members of + /// the computation expression call the check, and an inline member cannot call an internal function. + /// + /// + let inline isDefaultImmutableArray (source : obj) : bool = + let sourceType = source.GetType () + + sourceType.IsGenericType + && Type.(=) (sourceType.GetGenericTypeDefinition (), typedefof>) + && unbox((nonNull (sourceType.GetProperty "IsDefault")).GetValue source) + + /// + /// Throws for a sequence and for a default + /// that is given as a sequence. + /// + /// + /// + /// A default immutable array converts to a sequence without a check, and the sequence fails with + /// when a function enumerates it. A function that takes a sequence + /// calls this check, which treats a default immutable array as FSharp.Core treats a sequence + /// (ADR 0001, D6). Any other sequence passes. + /// + /// + /// An immutable array of the element type is recognized by its type, without reflection. A default immutable array of + /// a derived reference type, which arrives as a sequence of a base type by covariance, is recognized by its generic + /// type and by reading IsDefault through reflection. + /// + /// + let inline checkNotNullOrDefaultSeq name (source : seq<'T>) = + checkNotNull name source + + match source with + | :? ImmutableArray<'T> as array -> checkNotDefault name array + | _ -> + if isDefaultImmutableArray source then + nullArg name + + /// + /// The array under an immutable array, without a copy (ADR 0001, D5). + /// + /// + /// The caller reads the array and never writes to it: the immutable array and every value that shares it see the same + /// memory. A default immutable array throws for . + /// + let inline unwrapArray name (array : ImmutableArray<'T>) : 'T array = + checkNotDefault name array + // The check above guarantees an underlying array + ImmutableCollectionsMarshal.AsArray array + |> Unchecked.nonNull + + /// + /// An immutable array over an array, without a copy (ADR 0001, D5). + /// + /// + /// Only an array that nobody can write to any more may be wrapped: a fresh result of a computation. + /// + let inline wrapArray (array : 'T array) : ImmutableArray<'T> = ImmutableCollectionsMarshal.AsImmutableArray array + + /// The constructor of a struct pair, for the algorithms shared by the default and the `Tuple` modules. + let inline structPair (first : 'T1) (second : 'T2) = struct (first, second) + + /// The constructor of a reference pair, for the algorithms shared by the default and the `Tuple` modules. + let inline referencePair (first : 'T1) (second : 'T2) = (first, second) + + /// The constructor of a struct triple, for the algorithms shared by the default and the `Tuple` modules. + let inline structTriple (first : 'T1) (second : 'T2) (third : 'T3) = struct (first, second, third) + + /// The constructor of a reference triple, for the algorithms shared by the default and the `Tuple` modules. + let inline referenceTriple (first : 'T1) (second : 'T2) (third : 'T3) = (first, second, third) + +/// The default comparers of the collections (ADR 0001, D5). +[] +module internal Comparers = + + /// + /// The comparer of F# generic comparison for a type: the comparer of the sorts, of binarySearch and of the + /// sorted collections that are created without a comparer. + /// + /// + /// + /// The class resolves once per type, in + /// generic code, where FSharp.Core returns the comparer that it keeps for the type. + /// + /// + /// A call of FastGenericComparer at a concrete type that is not primitive creates a new comparer each time, and + /// the F# optimizer creates such calls when it inlines a small function of the library into its caller. Collections of + /// one type would then have different comparers, and the BCL could not take its shortcuts for two collections with the + /// same comparer, such as the one of ImmutableSortedSet.Union. A static field cannot be inlined. + /// + /// + [] + type DefaultComparer<'T when 'T : comparison> private () = + + static let comparer = LanguagePrimitives.FastGenericComparer<'T> + + /// The comparer of F# generic comparison for the type. + static member Instance : IComparer<'T> = comparer + +/// +/// The exception messages of FSharp.Core (FSCore.resx) and of the .NET libraries, in English. +/// +/// +/// The modules throw what the FSharp.Core function that they mirror throws (ADR 0001, D6). FSharp.Core keeps its resources +/// internal, so the texts are copied. The texts of the .NET libraries are for the functions that report a BCL exception +/// under the name of their own parameter. +/// module internal ErrorStrings = + + [] + let ArraysHadDifferentLengths = "The arrays have different lengths." + + [] + let IndexOutOfBounds = "The index was outside the range of elements in the list." + + [] + let InputArrayEmpty = "The input array was empty." + + [] + let InputListWasEmpty = "The input list was empty." + [] let InputMustBeNonNegative = "The input must be non-negative." + [] + let InputMustBePositive = "The input must be positive." + + [] + let InputSequenceEmpty = "The input sequence was empty." + + [] + let InputSequenceTooLong = "The input sequence contains more than one element." + + [] + let KeyNotFound = "The item, key, or index was not found in the collection." + + [] + let KeyNotFoundAlt = "An index satisfying the predicate was not found in the collection." + + [] + let ListsHadDifferentLengths = "The lists had different lengths." + + /// The message of the modules written before ADR 0001. [] let ListsHaveDifferentLengths = "The lists have different lengths." + + /// The message of the .NET libraries for a negative argument, as `Array.BinarySearch` reports it. + [] + let NeedNonNegativeNumber = "Non-negative number required." + + [] + let NotAPermutation = "The function did not compute a permutation." + + [] + let NotEnoughElements = "The input sequence has an insufficient number of elements." + + [] + let OutOfRange = "The index is outside the legal range." + +/// +/// The exceptions of the DetailedExceptions module of FSharp.Core, with the same types, messages and parameter names. +/// +/// +/// The callers pass the names of their own parameters (ADR 0001, D6). +/// +module internal Raise = + + let inline private invalidArgFormat (arg : string) (format : string) (paramArray : obj array) = + raise (ArgumentException (String.Format (format, paramArray), arg)) + + /// The arrays of a function of two arrays have different lengths. + let invalidArgDifferentArrayLength (arg1 : string) (len1 : int) (arg2 : string) (len2 : int) : 'T = + invalidArgFormat arg1 "{0}\n{1}.Length = {2}, {3}.Length = {4}" [| + ErrorStrings.ArraysHadDifferentLengths + arg1 + len1 + arg2 + len2 + |] + + /// The lists of a function of two lists have different lengths; `diff` is how much shorter `arg1` is. + let invalidArgDifferentListLength (arg1 : string) (arg2 : string) (diff : int) : 'T = + invalidArgFormat arg1 "{0}\n{1} is {2} {3} shorter than {4}" [| + ErrorStrings.ListsHadDifferentLengths + arg1 + diff + (if diff = 1 then "element" else "elements") + arg2 + |] + + /// The arrays of a function of three arrays have different lengths. + let invalidArg3ArraysDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = + invalidArgFormat + (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) + "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" + [| ErrorStrings.ArraysHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + + /// The lists of a function of three lists have different lengths. + let invalidArg3ListsDifferent (arg1 : string) (arg2 : string) (arg3 : string) (len1 : int) (len2 : int) (len3 : int) : 'T = + invalidArgFormat + (String.Concat [| arg1; ", "; arg2; ", "; arg3 |]) + "{0}\n {1}.Length = {2}, {3}.Length = {4}, {5}.Length = {6}" + [| ErrorStrings.ListsHadDifferentLengths; arg1; len1; arg2; len2; arg3; len3 |] + + /// A count or an index is negative. + let invalidArgInputMustBeNonNegative (arg : string) (count : int) : 'T = + invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBeNonNegative; arg; count |] + + /// A size is not positive. + let invalidArgInputMustBePositive (arg : string) (count : int) : 'T = + invalidArgFormat arg "{0}\n{1} = {2}" [| ErrorStrings.InputMustBePositive; arg; count |] + + /// An index or a count is beyond a bound. + let invalidArgOutOfRange (arg : string) (index : int) (text : string) (bound : int) : 'T = + let message = + String.Format ("{0}\n{1} = {2}, {3} = {4}", [| box ErrorStrings.OutOfRange; arg; index; text; bound |]) + + raise (ArgumentOutOfRangeException (arg, message)) + + /// A list is shorter than an index. + let invalidOpListNotEnoughElements (index : int) : 'T = + let message = + String.Format ( + "{0}\nThe list was {1} {2} shorter than the index", + [| box ErrorStrings.NotEnoughElements; index; (if index = 1 then "element" else "elements") |] + ) + + raise (InvalidOperationException message) + + /// No element satisfies the predicate of a `find` or `pick` function. + let indexNotFound () : 'T = raise (KeyNotFoundException ErrorStrings.KeyNotFoundAlt) + + /// A collection has fewer elements than a function needs. + let notEnoughElements () : 'T = raise (InvalidOperationException ErrorStrings.NotEnoughElements) + +/// The checks of the FSharp.Core Array module that the functions of several arrays share. +module internal ArrayChecks = + + /// The arrays of a function of two arrays must have the same length. + let inline sameLengths (name1 : string) (array1 : 'T1 array) (name2 : string) (array2 : 'T2 array) = + if array1.Length <> array2.Length then + Raise.invalidArgDifferentArrayLength name1 array1.Length name2 array2.Length + + /// The arrays of a function of three arrays must have the same length. + let inline sameLengths3 + (name1 : string) + (array1 : 'T1 array) + (name2 : string) + (array2 : 'T2 array) + (name3 : string) + (array3 : 'T3 array) + = + if + array1.Length <> array2.Length + || array1.Length <> array3.Length + then + Raise.invalidArg3ArraysDifferent name1 name2 name3 array1.Length array2.Length array3.Length + +/// +/// Algorithms over arrays that the default modules and the Tuple modules share (ADR 0001, D3). +/// +/// +/// Each algorithm takes the constructor of the tuples that it produces, so that neither variant pays for a second pass. +/// The callers check their arguments first, with the exceptions of the FSharp.Core module that they mirror. +/// +module internal Arrays = + + /// The pairs of every element of `array1` with every element of `array2`. + let inline allPairs ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = + let length2 = array2.Length + let result = Array.zeroCreate (array1.Length * length2) + + for i = 0 to array1.Length - 1 do + for j = 0 to length2 - 1 do + result[i * length2 + j] <- tuple array1[i] array2[j] + + result + + /// A key that may be null, for a dictionary, which rejects null keys. + [] + type NullableKey<'Key> = { Key : 'Key } + + /// Compares wrapped keys in F# structural equality, which handles null keys, as FSharp.Core does for its keys. + let nullableKeyComparer<'Key when 'Key : equality> () = + let structural = HashIdentity.Structural<'Key> + + { + new IEqualityComparer> with + member _.Equals (left, right) = structural.Equals (left.Key, right.Key) + member _.GetHashCode key = structural.GetHashCode key.Key + } + + /// + /// The keys of the projection in the order of their first occurrence, each with the number of its elements. + /// + /// + /// The counting keeps one counter per key, not the elements, so it needs memory for the keys only. + /// + let inline countBy + ([] tuple : 'Key -> int -> 'R) + (projection : 'T -> 'Key) + (array : 'T array) + : 'R array when 'Key : equality = + let indices = Dictionary, int>(nullableKeyComparer<'Key>()) + let keys = ResizeArray<'Key>() + let counts = ResizeArray() + + for item in array do + let key = projection item + let mutable index = 0 + + if indices.TryGetValue ({ Key = key }, &index) then + counts[index] <- counts[index] + 1 + else + indices.Add ({ Key = key }, keys.Count) + keys.Add key + counts.Add 1 + + let result = Array.zeroCreate keys.Count + + for index = 0 to result.Length - 1 do + result[index] <- tuple keys[index] counts[index] + + result + + /// The keys of the projection in the order of their first occurrence, each with its elements in their order. + let inline groupBy + ([] tuple : 'Key -> 'T array -> 'R) + (projection : 'T -> 'Key) + (array : 'T array) + : 'R array when 'Key : equality = + let lookup = array.ToLookup ((fun item -> projection item), HashIdentity.Structural<'Key>) + let result = Array.zeroCreate lookup.Count + let mutable index = 0 + + for grouping in lookup do + result[index] <- tuple grouping.Key (grouping.ToArray ()) + index <- index + 1 + + result + + /// Every element with its index. + let inline indexed ([] tuple : int -> 'T -> 'R) (array : 'T array) : 'R array = + let result = Array.zeroCreate array.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple i array[i] + + result + + /// Every element with its successor. + let inline pairwise ([] tuple : 'T -> 'T -> 'R) (array : 'T array) : 'R array = + if array.Length < 2 then + Array.Empty () + else + let result = Array.zeroCreate (array.Length - 1) + + for i = 0 to result.Length - 1 do + result[i] <- tuple array[i] array[i + 1] + + result + + /// Splits the pairs of an array with `split` into two arrays. + let inline unzip ([] split : 'P -> struct ('T1 * 'T2)) (array : 'P array) = + let result1 = Array.zeroCreate array.Length + let result2 = Array.zeroCreate array.Length + + for i = 0 to array.Length - 1 do + let struct (first, second) = split array[i] + result1[i] <- first + result2[i] <- second + + struct (result1, result2) + + /// Splits the triples of an array with `split` into three arrays. + let inline unzip3 ([] split : 'P -> struct ('T1 * 'T2 * 'T3)) (array : 'P array) = + let result1 = Array.zeroCreate array.Length + let result2 = Array.zeroCreate array.Length + let result3 = Array.zeroCreate array.Length + + for i = 0 to array.Length - 1 do + let struct (first, second, third) = split array[i] + result1[i] <- first + result2[i] <- second + result3[i] <- third + + struct (result1, result2, result3) + + /// Pairs the elements of two arrays of the same length. + let inline zip ([] tuple : 'T1 -> 'T2 -> 'R) (array1 : 'T1 array) (array2 : 'T2 array) : 'R array = + let result = Array.zeroCreate array1.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple array1[i] array2[i] + + result + + /// Combines the elements of three arrays of the same length. + let inline zip3 + ([] tuple : 'T1 -> 'T2 -> 'T3 -> 'R) + (array1 : 'T1 array) + (array2 : 'T2 array) + (array3 : 'T3 array) + : 'R array = + let result = Array.zeroCreate array1.Length + + for i = 0 to result.Length - 1 do + result[i] <- tuple array1[i] array2[i] array3[i] + + result diff --git a/src/FSharp.Collections.Immutable/Seq.fs b/src/FSharp.Collections.Immutable/Seq.fs deleted file mode 100644 index d9826b10..00000000 --- a/src/FSharp.Collections.Immutable/Seq.fs +++ /dev/null @@ -1,32 +0,0 @@ -#if INTERACTIVE -namespace global -#else -namespace FSharp.Collections.Immutable -#endif - -[] -module Seq = - - let inline ofFlatList flatList = FlatList.toSeq flatList - let inline toFlatList seq = FlatList.ofSeq seq - - let inline ofStack stack = Stack.toSeq stack - let inline toStack seq = Stack.ofSeq seq - - let inline ofImmutableList immutableList = ImmutableList.toSeq immutableList - let inline toImmutableList seq = ImmutableList.ofSeq seq - - let inline ofQueue queue = Queue.toSeq queue - let inline toQueue queue = Queue.ofSeq queue - - let inline ofHashMap hashMap = HashMap.toSeq hashMap - let inline toHashMap hashMap = HashMap.ofSeq hashMap - - let inline ofSortedMap sortedHashMap = SortedMap.toSeq sortedHashMap - let inline toSortedMap sortedHashMap = SortedMap.ofSeq sortedHashMap - - let inline ofHashSet hashSet = HashSet.toSeq hashSet - let inline toHashSet hashSet = HashSet.ofSeq hashSet - - let inline ofSortedSet sortedSet = SortedSet.toSeq sortedSet - let inline toSortedSet sortedSet = SortedSet.ofSeq sortedSet diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/ContractBase.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/ContractBase.fs new file mode 100644 index 00000000..e00217ce --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/ContractBase.fs @@ -0,0 +1,25 @@ +namespace FSharp.Collections.Immutable.Tests + +/// The helpers that every contract base shares. +/// The name of the module under test, for the messages of the assertions. +/// The FSharp.Core module whose exceptions the module mirrors. +/// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. +[] +type ContractBase (name : string, mirror : Mirror, paramName : string -> string) = + + /// The result of the oracle of the FSharp.Core module that the module mirrors. + member _.Mirrored (arrayOracle : unit -> 'R, listOracle : unit -> 'R) = + match mirror with + | ArrayMirror -> arrayOracle () + | ListMirror -> listOracle () + + /// Asserts that a function of the module throws what the same function of the mirrored FSharp.Core module throws. + member _.ThrowsLikeFSharpCore (arrayOracle : unit -> 'A, listOracle : unit -> 'L, actual : unit -> 'R, message : string) = + ExceptionAssert.Mirrored ( + mirror, + paramName, + arrayOracle >> ignore, + listOracle >> ignore, + actual >> ignore, + $"%s{name}: %s{message}" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs new file mode 100644 index 00000000..46f0d8d2 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/IndexedContract.fs @@ -0,0 +1,294 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the index-based functions of FlatList, IndexedList and IIndexedList +/// (ADR 0001, Appendix A). +/// +/// +/// The tests run on the input with several elements; the indices of their rows are computed from its length. +/// +[] +type IndexedContract<'C> (ops : IndexedOps<'C>) = + inherit ContractBase (ops.Name, ops.Mirror, ops.ParamName) + + /// An equality comparer that ignores the sign, for the functions that take a comparer. + static let absoluteEquality = { + new IEqualityComparer with + member _.Equals (left, right) = abs left = abs right + member _.GetHashCode item = abs item + } + + /// The collection of the input with several elements and the elements that it enumerates. + let several () = + let collection = ops.OfSeq Inputs.Several + struct (collection, ops.ToArray collection) + + /// Every index of an element. + static member Indices = seq { 0 .. Inputs.Several.Length - 1 } + + /// The indices next to the elements: before the first one and after the last one. + static member IndicesOutOfRange = seq { + -1 + Inputs.Several.Length + } + + /// Every index of an element and the indices next to them. + static member IndicesAndOutOfRange = seq { -1 .. Inputs.Several.Length } + + /// The indices to insert at: before the first element, inside and after the last one. + static member InsertionIndices = seq { + 0 + 3 + Inputs.Several.Length + } + + /// The indices to insert at that are out of range: before the first element and beyond the one after the last. + static member InsertionIndicesOutOfRange = seq { + -1 + Inputs.Several.Length + 1 + } + + /// Each index to insert at with no values and with two values. + static member InsertionIndicesAndValues = seq { + for index in IndexedContract<'C>.InsertionIndices do + for values in [| [||]; [| 98; 99 |] |] do + struct (index, values) + } + + /// The indices to remove at: the first element, one inside and the last one. + static member RemovalIndices = seq { + 0 + 3 + Inputs.Several.Length - 1 + } + + /// The ranges to remove, as an index and a count: from the first element, inside, the last element, and no element. + static member RemovalRanges = seq { + struct (0, 2) + struct (3, 3) + struct (Inputs.Several.Length - 1, 1) + struct (2, 0) + } + + /// The ranges that are out of range, as an index and a count: before the first element, beyond the last one, and a + /// negative count. + static member RemovalRangesOutOfRange = seq { + struct (-1, 1) + struct (Inputs.Several.Length - 1, 2) + struct (0, -1) + } + + /// The indices of the first and the last element. + static member FirstAndLastIndices = seq { + 0 + Inputs.Several.Length - 1 + } + + [.Indices)>] + member _.``item returns the element at the index`` (index : int) = + let struct (collection, elements) = several () + Assert.AreEqual( + Array.item index elements, + ops.Item index collection, + $"%s{ops.Name}.item must return the element at the index" + ) + + [.IndicesOutOfRange)>] + member this.``item throws for an index out of range as FSharp.Core does`` (index : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.item index elements), + (fun () -> List.item index (List.ofArray elements)), + (fun () -> ops.Item index collection), + "item at an index out of range must throw" + ) + + [.IndicesAndOutOfRange)>] + member _.``tryItem returns the element at the index or nothing`` (index : int) = + let struct (collection, elements) = several () + + Assert.AreEqual( + Array.tryItem index elements |> ValueOption.ofOption, + ops.TryItem index collection, + $"%s{ops.Name}.tryItem must return the element at the index or nothing" + ) + + [] + member _.``indexOf returns the index of the first equal element or -1`` (value : int) = + let struct (collection, elements) = several () + + Assert.AreEqual( + Array.tryFindIndex ((=) value) elements + |> Option.defaultValue -1, + ops.IndexOf value collection, + $"%s{ops.Name}.indexOf must return the index of the first equal element or -1" + ) + + [] + member _.``indexOfWith compares with the equality comparer`` (value : int) = + let struct (collection, elements) = several () + + Assert.AreEqual( + Array.tryFindIndex (fun item -> abs item = abs value) elements + |> Option.defaultValue -1, + ops.IndexOfWith absoluteEquality value collection, + $"%s{ops.Name}.indexOfWith must compare with the equality comparer" + ) + + [] + member _.``lastIndexOf returns the index of the last equal element or -1`` (value : int) = + let struct (collection, elements) = several () + + Assert.AreEqual( + Array.tryFindIndexBack ((=) value) elements + |> Option.defaultValue -1, + ops.LastIndexOf value collection, + $"%s{ops.Name}.lastIndexOf must return the index of the last equal element or -1" + ) + + [] + member _.``lastIndexOf returns -1 in an empty collection`` () = + Assert.AreEqual( + -1, + ops.LastIndexOf 5 (ops.OfSeq Inputs.Empty), + $"%s{ops.Name}.lastIndexOf must return -1 in an empty collection" + ) + + [] + member _.``lastIndexOfWith compares with the equality comparer`` (value : int) = + let struct (collection, elements) = several () + + Assert.AreEqual( + Array.tryFindIndexBack (fun item -> abs item = abs value) elements + |> Option.defaultValue -1, + ops.LastIndexOfWith absoluteEquality value collection, + $"%s{ops.Name}.lastIndexOfWith must compare with the equality comparer" + ) + + [] + member _.``lastIndexOfWith returns -1 in an empty collection`` () = + Assert.AreEqual( + -1, + ops.LastIndexOfWith absoluteEquality 5 (ops.OfSeq Inputs.Empty), + $"%s{ops.Name}.lastIndexOfWith must return -1 in an empty collection" + ) + + [)>] + member _.``add appends the value`` (input : int array) = + let collection = ops.OfSeq input + + CollectionAssert.AreEqual ( + Array.append (ops.ToArray collection) [| 99 |], + ops.ToArray (ops.Add 99 collection), + $"%s{ops.Name}.add must append the value" + ) + + [.InsertionIndices)>] + member _.``insertAt inserts the value at the index`` (index : int) = + let struct (collection, elements) = several () + + CollectionAssert.AreEqual ( + Array.insertAt index 99 elements, + ops.ToArray (ops.InsertAt index 99 collection), + $"%s{ops.Name}.insertAt must insert the value at the index" + ) + + [.InsertionIndicesOutOfRange)>] + member this.``insertAt throws for an index out of range as FSharp.Core does`` (index : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.insertAt index 99 elements), + (fun () -> List.insertAt index 99 (List.ofArray elements)), + (fun () -> ops.InsertAt index 99 collection), + "insertAt at an index out of range must throw" + ) + + [.InsertionIndicesAndValues)>] + member _.``insertManyAt inserts the values at the index`` (index : int, values : int array) = + let struct (collection, elements) = several () + + CollectionAssert.AreEqual ( + Array.insertManyAt index values elements, + ops.ToArray (ops.InsertManyAt index values collection), + $"%s{ops.Name}.insertManyAt must insert the values at the index" + ) + + [.InsertionIndicesOutOfRange)>] + member this.``insertManyAt throws for an index out of range as FSharp.Core does`` (index : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.insertManyAt index [ 99 ] elements), + (fun () -> List.insertManyAt index [ 99 ] (List.ofArray elements)), + (fun () -> ops.InsertManyAt index [ 99 ] collection), + "insertManyAt at an index out of range must throw" + ) + + [.RemovalIndices)>] + member _.``removeAt removes the element at the index`` (index : int) = + let struct (collection, elements) = several () + + CollectionAssert.AreEqual ( + Array.removeAt index elements, + ops.ToArray (ops.RemoveAt index collection), + $"%s{ops.Name}.removeAt must remove the element at the index" + ) + + [.IndicesOutOfRange)>] + member this.``removeAt throws for an index out of range as FSharp.Core does`` (index : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.removeAt index elements), + (fun () -> List.removeAt index (List.ofArray elements)), + (fun () -> ops.RemoveAt index collection), + "removeAt at an index out of range must throw" + ) + + [.RemovalRanges)>] + member _.``removeManyAt removes the elements of the range`` (index : int, count : int) = + let struct (collection, elements) = several () + + CollectionAssert.AreEqual ( + Array.removeManyAt index count elements, + ops.ToArray (ops.RemoveManyAt index count collection), + $"%s{ops.Name}.removeManyAt must remove the elements of the range" + ) + + [.RemovalRangesOutOfRange)>] + member this.``removeManyAt throws for a range out of range as FSharp.Core does`` (index : int, count : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.removeManyAt index count elements), + (fun () -> List.removeManyAt index count (List.ofArray elements)), + (fun () -> ops.RemoveManyAt index count collection), + "removeManyAt of a range out of range must throw" + ) + + [.FirstAndLastIndices)>] + member _.``updateAt replaces the element at the index`` (index : int) = + let struct (collection, elements) = several () + + CollectionAssert.AreEqual ( + Array.updateAt index 99 elements, + ops.ToArray (ops.UpdateAt index 99 collection), + $"%s{ops.Name}.updateAt must replace the element at the index" + ) + + [.IndicesOutOfRange)>] + member this.``updateAt throws for an index out of range as FSharp.Core does`` (index : int) = + let struct (collection, elements) = several () + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.updateAt index 99 elements), + (fun () -> List.updateAt index 99 (List.ofArray elements)), + (fun () -> ops.UpdateAt index 99 collection), + "updateAt at an index out of range must throw" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs new file mode 100644 index 00000000..1169acd3 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqCreationContract.fs @@ -0,0 +1,143 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the creation functions of the concrete sequence-like modules: FlatList, IndexedList, +/// Queue and Stack (ADR 0001, Appendix A). +/// +[] +type SeqCreationContract<'C> (ops : SeqCreationOps<'C>) = + inherit ContractBase (ops.Name, ops.Mirror, ops.ParamName) + + [] + member _.``empty has no elements`` () = + Assert.IsEmpty (ops.ToArray (ops.Empty ()), $"%s{ops.Name}.empty must have no elements") + + [] + member _.``init creates the elements in index order`` (count : int) = + CollectionAssert.AreEqual ( + Array.init count (fun index -> index * index), + ops.ToArray (ops.Init count (fun index -> index * index)), + $"%s{ops.Name}.init must create the elements in index order" + ) + + [] + member this.``init throws for a negative count as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.init -1 id), + (fun () -> List.init -1 id), + (fun () -> ops.Init -1 id), + "init -1 must throw" + ) + + [)>] + member _.``ofArray copies the elements in order`` (input : int array) = + // The test changes the source after the creation, so it changes a copy of its row + let source = Array.copy input + let collection = ops.OfArray source + + if source.Length > 0 then + source[0] <- 1000 + + CollectionAssert.AreEqual (input, ops.ToArray collection, $"%s{ops.Name}.ofArray must copy the elements in order") + + [] + member this.``ofArray throws for a null array as FSharp.Core does`` () = + let source = Unchecked.defaultof + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.copy source), + (fun () -> List.ofArray source), + (fun () -> ops.OfArray source), + "ofArray of a null array must throw" + ) + + [)>] + member _.``ofList keeps the elements in order`` (input : int array) = + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfList (List.ofArray input)), + $"%s{ops.Name}.ofList must keep the elements in order" + ) + + [)>] + member _.``ofReadOnlySpan copies the elements in order`` (input : int array) = + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfReadOnlySpan input), + $"%s{ops.Name}.ofReadOnlySpan must copy the elements in order" + ) + + [)>] + member _.``ofSeq keeps the elements in order`` (input : int array) = + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfSeq (List.ofArray input)), + $"%s{ops.Name}.ofSeq of a list must keep the elements in order" + ) + + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfSeq input), + $"%s{ops.Name}.ofSeq of an array must keep the elements in order" + ) + + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfSeq (Seq.map id input)), + $"%s{ops.Name}.ofSeq of a lazy sequence must keep the elements in order" + ) + + [] + member this.``ofSeq throws for a null sequence as FSharp.Core does`` () = + let source = Unchecked.defaultof> + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.ofSeq source), + (fun () -> List.ofSeq source), + (fun () -> ops.OfSeq source), + "ofSeq of a null sequence must throw" + ) + + [)>] + member _.``ofSpan copies the elements in order`` (input : int array) = + CollectionAssert.AreEqual (input, ops.ToArray (ops.OfSpan input), $"%s{ops.Name}.ofSpan must copy the elements in order") + + [] + member _.``replicate repeats the value`` (count : int) = + CollectionAssert.AreEqual ( + Array.replicate count 7, + ops.ToArray (ops.Replicate count 7), + $"%s{ops.Name}.replicate must repeat the value" + ) + + [] + member this.``replicate throws for a negative count as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.replicate -1 7), + (fun () -> List.replicate -1 7), + (fun () -> ops.Replicate -1 7), + "replicate -1 must throw" + ) + + [] + member _.``singleton has one element`` () = + CollectionAssert.AreEqual ([| 42 |], ops.ToArray (ops.Singleton 42), $"%s{ops.Name}.singleton must have one element") + + [] + member _.``unfold generates the elements until the generator stops`` (start : int) = + let generator (state : int) = + if state > 0 then + ValueSome (struct (state * 10, state - 1)) + else + ValueNone + + let generatorOption (state : int) = if state > 0 then Some (state * 10, state - 1) else None + + CollectionAssert.AreEqual ( + Array.unfold generatorOption start, + ops.ToArray (ops.Unfold generator start), + $"%s{ops.Name}.unfold must generate the elements until the generator stops" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeAccessContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeAccessContract.fs new file mode 100644 index 00000000..8fee7d72 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeAccessContract.fs @@ -0,0 +1,167 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The helpers that the contract bases of the sequence-like modules share. +/// +/// +/// The contract tests compute their expectations with the FSharp.Core Array module over the elements in the order +/// in which the collection enumerates them, so one test body serves FIFO, LIFO and indexed collections (ADR 0001, D8). +/// +[] +type SeqContractBase<'C> (ops : SeqOps<'C>) = + inherit ContractBase (ops.Name, ops.Mirror, ops.ParamName) + + /// The collection of the elements of an input and the elements in the order in which the collection enumerates them. + member _.Create (input : int array) = + let collection = ops.OfSeq input + struct (collection, ops.ToArray collection) + +/// The contract of the properties and the access functions of a sequence-like module. +[] +type SeqLikeAccessContract<'C> (ops : SeqOps<'C>) = + inherit SeqContractBase<'C> (ops) + + /// The inputs that do not have exactly one element. + static member NotSingletons = seq { + Inputs.Empty + Inputs.Several + } + + // ---- Properties ---- + + [)>] + member this.``isEmpty is true only for a collection without elements`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.isEmpty elements, + ops.IsEmpty collection, + $"%s{ops.Name}.isEmpty must be true only for a collection without elements" + ) + + [)>] + member this.``length counts the elements`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.length elements, ops.Length collection, $"%s{ops.Name}.length must count the elements") + + // ---- Access ---- + + [] + member _.``exactlyOne returns the only element`` () = + let collection = ops.OfSeq Inputs.Singleton + + Assert.AreEqual( + Array.exactlyOne Inputs.Singleton, + ops.ExactlyOne collection, + $"%s{ops.Name}.exactlyOne must return the only element" + ) + + [.NotSingletons)>] + member this.``exactlyOne throws for no element or several elements as FSharp.Core does`` (input : int array) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.exactlyOne input), + (fun () -> List.exactlyOne (List.ofArray input)), + (fun () -> ops.ExactlyOne (ops.OfSeq input)), + "exactlyOne of no element or of several elements must throw" + ) + + [)>] + member this.``head returns the first element`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.head elements, ops.Head collection, $"%s{ops.Name}.head must return the first element") + + [] + member this.``head throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.head Array.empty), + (fun () -> List.head List.empty), + (fun () -> ops.Head (ops.OfSeq Inputs.Empty)), + "head of an empty collection must throw" + ) + + [)>] + member this.``last returns the last element`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.last elements, ops.Last collection, $"%s{ops.Name}.last must return the last element") + + [] + member this.``last throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.last Array.empty), + (fun () -> List.last List.empty), + (fun () -> ops.Last (ops.OfSeq Inputs.Empty)), + "last of an empty collection must throw" + ) + + [)>] + member this.``tail returns the elements after the first one`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.tail elements, + ops.ToArray (ops.Tail collection), + $"%s{ops.Name}.tail must return the elements after the first one" + ) + + [] + member this.``tail throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.tail Array.empty), + (fun () -> List.tail List.empty), + (fun () -> ops.Tail (ops.OfSeq Inputs.Empty)), + "tail of an empty collection must throw" + ) + + [)>] + member this.``tryExactlyOne returns the only element or nothing`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryExactlyOne elements |> ValueOption.ofOption, + ops.TryExactlyOne collection, + $"%s{ops.Name}.tryExactlyOne must return the only element or nothing" + ) + + [)>] + member this.``tryHead returns the first element or nothing`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryHead elements |> ValueOption.ofOption, + ops.TryHead collection, + $"%s{ops.Name}.tryHead must return the first element or nothing" + ) + + [)>] + member this.``tryLast returns the last element or nothing`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryLast elements |> ValueOption.ofOption, + ops.TryLast collection, + $"%s{ops.Name}.tryLast must return the last element or nothing" + ) + + [)>] + member this.``tryTail returns the elements after the first one or nothing`` (input : int array) = + let struct (collection, elements) = this.Create input + let tail = ops.TryTail collection + + Assert.AreEqual( + not (Array.isEmpty elements), + tail.IsSome, + $"%s{ops.Name}.tryTail must return a tail exactly for a collection with elements" + ) + + // CollectionAssert compares the elements of the tail and reports the first one that differs + match tail with + | ValueSome tail -> + CollectionAssert.AreEqual ( + Array.tail elements, + ops.ToArray tail, + $"%s{ops.Name}.tryTail must return the elements after the first one" + ) + | ValueNone -> () diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeContract.fs new file mode 100644 index 00000000..613eeee7 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeContract.fs @@ -0,0 +1,57 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the functions that every sequence-like module provides: FlatList, IndexedList, +/// IIndexedList, Queue, IQueue, Stack and IStack (ADR 0001, Appendix A). +/// +/// +/// The facet is a chain of abstract classes, one file per category; a collection derives one test class from this last one. +/// +[] +type SeqLikeContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeRandomContract<'C> (ops) + + [)>] + member _.``toArray returns the elements in order`` (input : int array) = + // The other tests observe a collection with toArray, so toArray is checked against the sequence that the collection + // was created from, which ofSeq keeps in its order + CollectionAssert.AreEqual ( + input, + ops.ToArray (ops.OfSeq input), + $"%s{ops.Name}.toArray must return the elements in the order of the sequence that the collection was created from" + ) + + [] + member _.``toArray returns a new array`` () = + let collection = ops.OfSeq Inputs.Several + let array = ops.ToArray collection + array[0] <- 1000 + + CollectionAssert.AreEqual ( + Inputs.Several, + ops.ToArray collection, + $"%s{ops.Name}.toArray must return a copy that does not change the collection" + ) + + [)>] + member this.``toList returns the elements in order`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + elements, + List.toArray (ops.ToList collection), + $"%s{ops.Name}.toList must return the elements in the order in which the collection enumerates them" + ) + + [)>] + member this.``toSeq returns the elements in order`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + elements, + Seq.toArray (ops.ToSeq collection), + $"%s{ops.Name}.toSeq must return the elements in the order in which the collection enumerates them" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeFoldingContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeFoldingContract.fs new file mode 100644 index 00000000..308cdf50 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeFoldingContract.fs @@ -0,0 +1,363 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The contract of the folding functions of a sequence-like module. +[] +type SeqLikeFoldingContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeTransformationContract<'C> (ops) + + /// A folder whose state records the order in which it saw the elements. + static let record (visited : int array) (item : int) = Array.append visited [| item |] + + /// A folder from the end whose state records the order in which it saw the elements. + static let recordBack (item : int) (visited : int array) = Array.append visited [| item |] + + /// Every input with every input, with a prefix of the input with several elements and with an input that differs from + /// that one at the second element. + static member ComparedInputs = + let others = seq { + yield! Inputs.All + [| 5; -3 |] + [| 5; 0 |] + } + + seq { + for first in Inputs.All do + for second in others do + struct (first, second) + } + + [] + member _.``average returns the average of the elements`` (input : float array) = + Assert.AreEqual( + Array.average input, + ops.Average input, + $"%s{ops.Name}.average must return the average of the elements" + ) + + [] + member this.``average throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.average Array.empty), + (fun () -> List.average List.empty), + (fun () -> ops.Average Array.empty), + "average of an empty collection must throw" + ) + + [)>] + member this.``averageBy returns the average of the projections`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.averageBy float elements, + ops.AverageBy float collection, + $"%s{ops.Name}.averageBy must return the average of the projections" + ) + + [] + member this.``averageBy throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.averageBy float Array.empty), + (fun () -> List.averageBy float List.empty), + (fun () -> ops.AverageBy float (ops.OfSeq Inputs.Empty)), + "averageBy of an empty collection must throw" + ) + + [.ComparedInputs)>] + member this.``compareWith compares element by element and then by length`` (first : int array, second : int array) = + let struct (collection, elements) = this.Create first + let struct (other, otherElements) = this.Create second + + Assert.AreEqual( + Array.compareWith compare elements otherElements |> sign, + ops.CompareWith compare collection other |> sign, + $"%s{ops.Name}.compareWith must compare element by element and then by length" + ) + + [)>] + member this.``fold visits the elements from the first one`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.fold record [||] elements, + ops.Fold record [||] collection, + $"%s{ops.Name}.fold must visit the elements from the first one" + ) + + [] + member this.``fold2 visits the pairs from the first index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let folder (visited : int array) a b = Array.append visited [| a * 100 + b |] + + CollectionAssert.AreEqual ( + Array.fold2 folder [||] elements otherElements, + ops.Fold2 folder [||] collection other, + $"%s{ops.Name}.fold2 must visit the pairs from the first index" + ) + + [] + member this.``fold2 throws for different lengths as FSharp.Core does`` () = + let folder (state : int array) (_ : int) (_ : int) = state + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.fold2 folder [||] Inputs.Singleton Inputs.Several), + (fun () -> List.fold2 folder [||] (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Fold2 folder [||] (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "fold2 of collections of different lengths must throw" + ) + + [)>] + member this.``foldBack visits the elements from the last one`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.foldBack recordBack elements [||], + ops.FoldBack recordBack collection [||], + $"%s{ops.Name}.foldBack must visit the elements from the last one" + ) + + [] + member this.``foldBack2 visits the pairs from the last index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let folder a b (visited : int array) = Array.append visited [| a * 100 + b |] + + CollectionAssert.AreEqual ( + Array.foldBack2 folder elements otherElements [||], + ops.FoldBack2 folder collection other [||], + $"%s{ops.Name}.foldBack2 must visit the pairs from the last index" + ) + + [] + member this.``foldBack2 throws for different lengths as FSharp.Core does`` () = + let folder (_ : int) (_ : int) (state : int array) = state + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.foldBack2 folder Inputs.Singleton Inputs.Several [||]), + (fun () -> List.foldBack2 folder (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several) [||]), + (fun () -> ops.FoldBack2 folder (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several) [||]), + "foldBack2 of collections of different lengths must throw" + ) + + [)>] + member this.``iter visits every element in order`` (input : int array) = + let struct (collection, elements) = this.Create input + let visited = ResizeArray() + ops.Iter visited.Add collection + CollectionAssert.AreEqual (elements, visited.ToArray (), $"%s{ops.Name}.iter must visit every element in order") + + [] + member this.``iter2 visits every pair in order`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let visited = ResizeArray() + ops.Iter2 (fun a b -> visited.Add (struct (a, b))) collection other + + CollectionAssert.AreEqual ( + Array.zip elements otherElements + |> Array.map (fun (a, b) -> struct (a, b)), + visited.ToArray (), + $"%s{ops.Name}.iter2 must visit every pair in order" + ) + + [] + member this.``iter2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.iter2 (fun _ _ -> ()) Inputs.Singleton Inputs.Several), + (fun () -> List.iter2 (fun _ _ -> ()) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Iter2 (fun _ _ -> ()) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "iter2 of collections of different lengths must throw" + ) + + [)>] + member this.``iteri visits every element with its index`` (input : int array) = + let struct (collection, elements) = this.Create input + let visited = ResizeArray() + ops.Iteri (fun index item -> visited.Add (struct (index, item))) collection + + CollectionAssert.AreEqual ( + Array.indexed elements + |> Array.map (fun (index, item) -> struct (index, item)), + visited.ToArray (), + $"%s{ops.Name}.iteri must visit every element with its index" + ) + + [] + member this.``iteri2 visits every pair with its index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let visited = ResizeArray() + ops.Iteri2 (fun index a b -> visited.Add (struct (index, a, b))) collection other + + CollectionAssert.AreEqual ( + Array.zip elements otherElements + |> Array.mapi (fun index (a, b) -> struct (index, a, b)), + visited.ToArray (), + $"%s{ops.Name}.iteri2 must visit every pair with its index" + ) + + [] + member this.``iteri2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.iteri2 (fun _ _ _ -> ()) Inputs.Singleton Inputs.Several), + (fun () -> List.iteri2 (fun _ _ _ -> ()) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Iteri2 (fun _ _ _ -> ()) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "iteri2 of collections of different lengths must throw" + ) + + [)>] + member this.``mapFold maps the elements and threads the state from the first one`` (input : int array) = + let struct (collection, elements) = this.Create input + let expectedResults, expectedState = + Array.mapFold (fun state item -> item * state, state + 1) 1 elements + let struct (results, state) = + ops.MapFold (fun state item -> struct (item * state, state + 1)) 1 collection + CollectionAssert.AreEqual (expectedResults, results, $"%s{ops.Name}.mapFold must map the elements from the first one") + Assert.AreEqual(expectedState, state, $"%s{ops.Name}.mapFold must thread the state from the first element") + + [)>] + member this.``mapFoldBack maps the elements and threads the state from the last one`` (input : int array) = + let struct (collection, elements) = this.Create input + let expectedResults, expectedState = + Array.mapFoldBack (fun item state -> item * state, state + 1) elements 1 + let struct (results, state) = + ops.MapFoldBack (fun item state -> struct (item * state, state + 1)) collection 1 + CollectionAssert.AreEqual (expectedResults, results, $"%s{ops.Name}.mapFoldBack must map the elements from the last one") + Assert.AreEqual(expectedState, state, $"%s{ops.Name}.mapFoldBack must thread the state from the last element") + + [)>] + member this.``max returns the greatest element`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.max elements, ops.Max collection, $"%s{ops.Name}.max must return the greatest element") + + [] + member this.``max throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.max Array.empty), + (fun () -> List.max List.empty), + (fun () -> ops.Max (ops.OfSeq Inputs.Empty)), + "max of an empty collection must throw" + ) + + [)>] + member this.``maxBy returns the first element with the greatest projection`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.maxBy abs elements, + ops.MaxBy abs collection, + $"%s{ops.Name}.maxBy must return the first element with the greatest projection" + ) + + [] + member this.``maxBy throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.maxBy abs Array.empty), + (fun () -> List.maxBy abs List.empty), + (fun () -> ops.MaxBy abs (ops.OfSeq Inputs.Empty)), + "maxBy of an empty collection must throw" + ) + + [)>] + member this.``min returns the smallest element`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.min elements, ops.Min collection, $"%s{ops.Name}.min must return the smallest element") + + [] + member this.``min throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.min Array.empty), + (fun () -> List.min List.empty), + (fun () -> ops.Min (ops.OfSeq Inputs.Empty)), + "min of an empty collection must throw" + ) + + [)>] + member this.``minBy returns the first element with the smallest projection`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.minBy abs elements, + ops.MinBy abs collection, + $"%s{ops.Name}.minBy must return the first element with the smallest projection" + ) + + [] + member this.``minBy throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.minBy abs Array.empty), + (fun () -> List.minBy abs List.empty), + (fun () -> ops.MinBy abs (ops.OfSeq Inputs.Empty)), + "minBy of an empty collection must throw" + ) + + [)>] + member this.``reduce combines the elements from the first one`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.reduce (fun a b -> a * 2 - b) elements, + ops.Reduce (fun a b -> a * 2 - b) collection, + $"%s{ops.Name}.reduce must combine the elements from the first one" + ) + + [] + member this.``reduce throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.reduce (+) Array.empty), + (fun () -> List.reduce (+) List.empty), + (fun () -> ops.Reduce (+) (ops.OfSeq Inputs.Empty)), + "reduce of an empty collection must throw" + ) + + [)>] + member this.``reduceBack combines the elements from the last one`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.reduceBack (fun a b -> a * 2 - b) elements, + ops.ReduceBack (fun a b -> a * 2 - b) collection, + $"%s{ops.Name}.reduceBack must combine the elements from the last one" + ) + + [] + member this.``reduceBack throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.reduceBack (+) Array.empty), + (fun () -> List.reduceBack (+) List.empty), + (fun () -> ops.ReduceBack (+) (ops.OfSeq Inputs.Empty)), + "reduceBack of an empty collection must throw" + ) + + [)>] + member this.``scan returns every state from the first element`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.scan record [||] elements, + ops.Scan record [||] collection, + $"%s{ops.Name}.scan must return every state from the first element" + ) + + [)>] + member this.``scanBack returns every state from the last element`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.scanBack recordBack elements [||], + ops.ScanBack recordBack collection [||], + $"%s{ops.Name}.scanBack must return every state from the last element" + ) + + [)>] + member this.``sum adds the elements`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.sum elements, ops.Sum collection, $"%s{ops.Name}.sum must add the elements") + + [)>] + member this.``sumBy adds the projections`` (input : int array) = + let struct (collection, elements) = this.Create input + Assert.AreEqual(Array.sumBy abs elements, ops.SumBy abs collection, $"%s{ops.Name}.sumBy must add the projections") diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikePartitioningContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikePartitioningContract.fs new file mode 100644 index 00000000..ffc03149 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikePartitioningContract.fs @@ -0,0 +1,305 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The contract of the partitioning and grouping functions of a sequence-like module. +[] +type SeqLikePartitioningContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeFoldingContract<'C> (ops) + + /// The counts to try on a collection of the given length: negative, zero, inside and the whole length. + static let countsFor (length : int) = Array.distinct [| -1; 0; min 3 length; length |] + + /// Every input with each count to try on it: negative, zero, inside and the whole length. + static member InputsAndCounts = seq { + for input in Inputs.All do + for count in countsFor input.Length do + struct (input, count) + } + + /// Every input with each count from zero to its length: zero, inside and the whole length. + static member InputsAndCountsWithin = seq { + for input in Inputs.All do + for count in Array.distinct [| 0; min 3 input.Length; input.Length |] do + struct (input, count) + } + + /// Every input with each count to try on it and a count beyond its length. + static member InputsAndCountsBeyond = seq { + for input in Inputs.All do + for count in Array.append (countsFor input.Length) [| input.Length + 5 |] do + struct (input, count) + } + + /// Every input with each limit of the predicates of skipWhile and takeWhile: below every element, between them and + /// above every element. + static member InputsAndLimits = seq { + for input in Inputs.All do + for limit in [| -10; 6; 100 |] do + struct (input, limit) + } + + /// Every input with each number of parts: one, inside the length, the length and beyond it. + static member InputsAndPartCounts = seq { + for input in Inputs.All do + for count in [| 1; 3; 8; 9 |] do + struct (input, count) + } + + [)>] + member this.``countBy counts the elements of each key in the order of the first occurrence`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.countBy (fun item -> item % 3) elements + |> Array.map (fun (key, count) -> struct (key, count)), + ops.CountBy (fun item -> item % 3) collection, + $"%s{ops.Name}.countBy must count the elements of each key in the order of the first occurrence" + ) + + [)>] + member this.``distinct keeps the first occurrence of each element`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.distinct elements, + ops.ToArray (ops.Distinct collection), + $"%s{ops.Name}.distinct must keep the first occurrence of each element" + ) + + [)>] + member this.``distinctBy keeps the first element of each key`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.distinctBy abs elements, + ops.ToArray (ops.DistinctBy abs collection), + $"%s{ops.Name}.distinctBy must keep the first element of each key" + ) + + [)>] + member this.``except removes the excluded items and the duplicates`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.except [ 5; 12 ] elements, + ops.ToArray (ops.Except [ 5; 12 ] collection), + $"%s{ops.Name}.except must remove the excluded items and the duplicates" + ) + + [] + member this.``except throws for null items to exclude as FSharp.Core does`` () = + let excluded = Unchecked.defaultof> + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.except excluded Inputs.Several), + (fun () -> List.except excluded (List.ofArray Inputs.Several)), + (fun () -> ops.Except excluded (ops.OfSeq Inputs.Several)), + "except of null items must throw" + ) + + [)>] + member this.``groupBy groups the elements by key in the order of the first occurrence`` (input : int array) = + let struct (collection, elements) = this.Create input + let expected = Array.groupBy (fun item -> item % 3) elements + let groups = ops.GroupBy (fun item -> item % 3) collection + + // A group pairs its key with an array, which a pair compares by reference, so the keys and the elements are + // compared apart + CollectionAssert.AreEqual ( + Array.map fst expected, + groups |> Array.map (fun struct (key, _) -> key), + $"%s{ops.Name}.groupBy must return the keys in the order of their first occurrence" + ) + + CollectionAssert.AreEqual ( + Array.map snd expected, + groups |> Array.map (fun struct (_, items) -> items), + $"%s{ops.Name}.groupBy must group the elements of each key in their order" + ) + + [)>] + member this.``partition splits the elements by the predicate`` (input : int array) = + let struct (collection, elements) = this.Create input + let expectedAccepted, expectedRejected = Array.partition (fun item -> item > 0) elements + let struct (accepted, rejected) = ops.Partition (fun item -> item > 0) collection + + CollectionAssert.AreEqual ( + expectedAccepted, + ops.ToArray accepted, + $"%s{ops.Name}.partition must return the elements that satisfy the predicate" + ) + + CollectionAssert.AreEqual ( + expectedRejected, + ops.ToArray rejected, + $"%s{ops.Name}.partition must return the elements that do not satisfy the predicate" + ) + + [)>] + member this.``partitionWith splits the elements by the choice`` (input : int array) = + let struct (collection, elements) = this.Create input + + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + + let expectedFirst, expectedSecond = Array.partitionWith partitioner elements + let struct (first, second) = ops.PartitionWith partitioner collection + CollectionAssert.AreEqual (expectedFirst, first, $"%s{ops.Name}.partitionWith must return the first choices in order") + CollectionAssert.AreEqual (expectedSecond, second, $"%s{ops.Name}.partitionWith must return the second choices in order") + + [.InputsAndCounts)>] + member this.``skip drops the first elements`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.skip (max count 0) elements, + ops.ToArray (ops.Skip count collection), + $"%s{ops.Name}.skip must drop the first elements" + ) + + [] + member this.``skip throws for a count beyond the length as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.skip 9 Inputs.Several), + (fun () -> List.skip 9 (List.ofArray Inputs.Several)), + (fun () -> ops.Skip 9 (ops.OfSeq Inputs.Several)), + "skip beyond the length must throw" + ) + + [.InputsAndCounts)>] + member this.``skipLast drops the last elements`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.take (elements.Length - max count 0) elements, + ops.ToArray (ops.SkipLast count collection), + $"%s{ops.Name}.skipLast must drop the last elements" + ) + + [] + member this.``skipLast throws for a count beyond the length as skip does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.skip 9 Inputs.Several), + (fun () -> List.skip 9 (List.ofArray Inputs.Several)), + (fun () -> ops.SkipLast 9 (ops.OfSeq Inputs.Several)), + "skipLast beyond the length must throw" + ) + + [.InputsAndLimits)>] + member this.``skipWhile drops the first elements that satisfy the predicate`` (input : int array, limit : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.skipWhile (fun item -> item < limit) elements, + ops.ToArray (ops.SkipWhile (fun item -> item < limit) collection), + $"%s{ops.Name}.skipWhile must drop the first elements that satisfy the predicate" + ) + + [.InputsAndCountsWithin)>] + member this.``splitAt splits the elements at the index`` (input : int array, index : int) = + let struct (collection, elements) = this.Create input + let expectedBefore, expectedAfter = Array.splitAt index elements + let struct (before, after) = ops.SplitAt index collection + + CollectionAssert.AreEqual ( + expectedBefore, + ops.ToArray before, + $"%s{ops.Name}.splitAt must return the elements before the index first" + ) + + CollectionAssert.AreEqual ( + expectedAfter, + ops.ToArray after, + $"%s{ops.Name}.splitAt must return the elements from the index second" + ) + + [] + member this.``splitAt throws for a negative index or one beyond the length as FSharp.Core does`` (index : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.splitAt index Inputs.Several), + (fun () -> List.splitAt index (List.ofArray Inputs.Several)), + (fun () -> ops.SplitAt index (ops.OfSeq Inputs.Several)), + "splitAt at a negative index or beyond the length must throw" + ) + + [.InputsAndPartCounts)>] + member this.``splitInto splits the elements into parts of nearly equal length`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.splitInto count elements, + ops.SplitInto count collection, + $"%s{ops.Name}.splitInto must split the elements into parts of nearly equal length" + ) + + [] + member this.``splitInto throws for a count that is not positive as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.splitInto 0 Inputs.Several), + (fun () -> List.splitInto 0 (List.ofArray Inputs.Several)), + (fun () -> ops.SplitInto 0 (ops.OfSeq Inputs.Several)), + "splitInto 0 must throw" + ) + + [.InputsAndCountsWithin)>] + member this.``take returns the first elements`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.take count elements, + ops.ToArray (ops.Take count collection), + $"%s{ops.Name}.take must return the first elements" + ) + + [] + member this.``take throws for a negative count or one beyond the length as FSharp.Core does`` (count : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.take count Inputs.Several), + (fun () -> List.take count (List.ofArray Inputs.Several)), + (fun () -> ops.Take count (ops.OfSeq Inputs.Several)), + "take of a negative count or beyond the length must throw" + ) + + [.InputsAndCountsWithin)>] + member this.``takeLast returns the last elements`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.skip (elements.Length - count) elements, + ops.ToArray (ops.TakeLast count collection), + $"%s{ops.Name}.takeLast must return the last elements" + ) + + [] + member this.``takeLast throws for a negative count or one beyond the length as take does`` (count : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.take count Inputs.Several), + (fun () -> List.take count (List.ofArray Inputs.Several)), + (fun () -> ops.TakeLast count (ops.OfSeq Inputs.Several)), + "takeLast of a negative count or beyond the length must throw" + ) + + [.InputsAndLimits)>] + member this.``takeWhile returns the first elements that satisfy the predicate`` (input : int array, limit : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.takeWhile (fun item -> item < limit) elements, + ops.ToArray (ops.TakeWhile (fun item -> item < limit) collection), + $"%s{ops.Name}.takeWhile must return the first elements that satisfy the predicate" + ) + + [.InputsAndCountsBeyond)>] + member this.``truncate returns at most the given number of first elements`` (input : int array, count : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.truncate count elements, + ops.ToArray (ops.Truncate count collection), + $"%s{ops.Name}.truncate must return at most the given number of first elements" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeRandomContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeRandomContract.fs new file mode 100644 index 00000000..2e4e6da5 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeRandomContract.fs @@ -0,0 +1,264 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the random functions of a sequence-like module. +/// +/// +/// With a seeded generator or a fixed randomizer, a function returns what the function of the mirrored FSharp.Core module +/// returns for the same elements; with , the tests check the properties of the result. +/// +[] +type SeqLikeRandomContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeSortingContract<'C> (ops) + + static let seed = 42 + + /// A randomizer that returns the same sequence of numbers for every instance. + static let randomizer () = + let random = Random seed + fun () -> random.NextDouble () + + /// The elements of a result, sorted, for the comparison of multisets. + static let sorted (items : int array) = Array.sort items + + /// The counts and the inputs that randomChoices rejects: a negative count, and a count of an empty input. + static member ChoicesFailures = seq { + struct (-1, Inputs.Several) + struct (3, Inputs.Empty) + } + + [)>] + member this.``randomChoice returns an element`` (input : int array) = + let struct (collection, elements) = this.Create input + let chosen = ops.RandomChoice collection + Assert.Contains (chosen, elements, $"%s{ops.Name}.randomChoice must return an element of the collection") + + [] + member this.``randomChoice throws for an empty collection as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.randomChoice Array.empty), + (fun () -> List.randomChoice List.empty), + (fun () -> ops.RandomChoice (ops.OfSeq Inputs.Empty)), + "randomChoice of an empty collection must throw" + ) + + [)>] + member this.``randomChoiceBy chooses as FSharp.Core does with the same randomizer`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomChoiceBy (randomizer ()) elements), + fun () -> List.randomChoiceBy (randomizer ()) (List.ofArray elements) + ) + + Assert.AreEqual( + expected, + ops.RandomChoiceBy (randomizer ()) collection, + $"%s{ops.Name}.randomChoiceBy must choose as FSharp.Core does with the same randomizer" + ) + + [] + member this.``randomChoiceBy throws for a randomizer outside the range as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.randomChoiceBy (fun () -> 1.0) Inputs.Several), + (fun () -> List.randomChoiceBy (fun () -> 1.0) (List.ofArray Inputs.Several)), + (fun () -> ops.RandomChoiceBy (fun () -> 1.0) (ops.OfSeq Inputs.Several)), + "randomChoiceBy with a randomizer that returns 1.0 must throw" + ) + + [)>] + member this.``randomChoiceWith chooses as FSharp.Core does with the same generator`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomChoiceWith (Random seed) elements), + fun () -> List.randomChoiceWith (Random seed) (List.ofArray elements) + ) + + Assert.AreEqual( + expected, + ops.RandomChoiceWith (Random seed) collection, + $"%s{ops.Name}.randomChoiceWith must choose as FSharp.Core does with the same generator" + ) + + [] + member this.``randomChoiceWith throws for a null generator as FSharp.Core does`` () = + let random = Unchecked.defaultof + + this.ThrowsLikeFSharpCore ( + (fun () -> Array.randomChoiceWith random Inputs.Several), + (fun () -> List.randomChoiceWith random (List.ofArray Inputs.Several)), + (fun () -> ops.RandomChoiceWith random (ops.OfSeq Inputs.Several)), + "randomChoiceWith of a null generator must throw" + ) + + [)>] + member this.``randomChoices returns the given number of elements`` (input : int array) = + let struct (collection, elements) = this.Create input + let chosen = ops.ToArray (ops.RandomChoices 20 collection) + Assert.HasCount (20, chosen, $"%s{ops.Name}.randomChoices 20 must return 20 elements") + + for item in chosen do + Assert.Contains (item, elements, $"%s{ops.Name}.randomChoices must return elements of the collection") + + [.ChoicesFailures)>] + member this.``randomChoices throws for a negative count or an empty collection as FSharp.Core does`` + (count : int, input : int array) + = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.randomChoices count input), + (fun () -> List.randomChoices count (List.ofArray input)), + (fun () -> ops.RandomChoices count (ops.OfSeq input)), + "randomChoices of a negative count or of an empty collection must throw" + ) + + [)>] + member this.``randomChoicesBy chooses as FSharp.Core does with the same randomizer`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomChoicesBy (randomizer ()) 5 elements), + fun () -> + List.randomChoicesBy (randomizer ()) 5 (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomChoicesBy (randomizer ()) 5 collection), + $"%s{ops.Name}.randomChoicesBy must choose as FSharp.Core does with the same randomizer" + ) + + [)>] + member this.``randomChoicesWith chooses as FSharp.Core does with the same generator`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomChoicesWith (Random seed) 5 elements), + fun () -> + List.randomChoicesWith (Random seed) 5 (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomChoicesWith (Random seed) 5 collection), + $"%s{ops.Name}.randomChoicesWith must choose as FSharp.Core does with the same generator" + ) + + [)>] + member this.``randomSample returns elements at distinct indices`` (input : int array) = + let struct (collection, elements) = this.Create input + let count = min 3 elements.Length + let sample = ops.ToArray (ops.RandomSample count collection) + Assert.HasCount (count, sample, $"%s{ops.Name}.randomSample must return the given number of elements") + + // Every sampled element must be available in the input as often as it is sampled + let remaining = ResizeArray elements + + for item in sample do + Assert.IsTrue ( + remaining.Remove item, + $"%s{ops.Name}.randomSample must take each element of the collection once at most" + ) + + [] + member this.``randomSample throws for a negative count or one beyond the length as FSharp.Core does`` (count : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.randomSample count Inputs.Several), + (fun () -> List.randomSample count (List.ofArray Inputs.Several)), + (fun () -> ops.RandomSample count (ops.OfSeq Inputs.Several)), + "randomSample of a negative count or beyond the length must throw" + ) + + [)>] + member this.``randomSampleBy samples as FSharp.Core does with the same randomizer`` (input : int array) = + let struct (collection, elements) = this.Create input + let count = min 3 elements.Length + + let expected = + this.Mirrored ( + (fun () -> Array.randomSampleBy (randomizer ()) count elements), + fun () -> + List.randomSampleBy (randomizer ()) count (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomSampleBy (randomizer ()) count collection), + $"%s{ops.Name}.randomSampleBy must sample as FSharp.Core does with the same randomizer" + ) + + [)>] + member this.``randomSampleWith samples as FSharp.Core does with the same generator`` (input : int array) = + let struct (collection, elements) = this.Create input + let count = min 3 elements.Length + + let expected = + this.Mirrored ( + (fun () -> Array.randomSampleWith (Random seed) count elements), + fun () -> + List.randomSampleWith (Random seed) count (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomSampleWith (Random seed) count collection), + $"%s{ops.Name}.randomSampleWith must sample as FSharp.Core does with the same generator" + ) + + [)>] + member this.``randomShuffle returns a permutation of the elements`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + sorted elements, + sorted (ops.ToArray (ops.RandomShuffle collection)), + $"%s{ops.Name}.randomShuffle must keep the elements" + ) + + [)>] + member this.``randomShuffleBy shuffles as FSharp.Core does with the same randomizer`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomShuffleBy (randomizer ()) elements), + fun () -> + List.randomShuffleBy (randomizer ()) (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomShuffleBy (randomizer ()) collection), + $"%s{ops.Name}.randomShuffleBy must shuffle as FSharp.Core does with the same randomizer" + ) + + [)>] + member this.``randomShuffleWith shuffles as FSharp.Core does with the same generator`` (input : int array) = + let struct (collection, elements) = this.Create input + + let expected = + this.Mirrored ( + (fun () -> Array.randomShuffleWith (Random seed) elements), + fun () -> + List.randomShuffleWith (Random seed) (List.ofArray elements) + |> List.toArray + ) + + CollectionAssert.AreEqual ( + expected, + ops.ToArray (ops.RandomShuffleWith (Random seed) collection), + $"%s{ops.Name}.randomShuffleWith must shuffle as FSharp.Core does with the same generator" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSearchContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSearchContract.fs new file mode 100644 index 00000000..1d648ef6 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSearchContract.fs @@ -0,0 +1,345 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The predicates of the search tests over elements, as the cases of their rows. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes +/// the function of the case. +/// +[] +type ElementPredicate = + /// The even elements. + | IsEven + /// The negative elements. + | IsNegative + /// The elements below a hundred, which are all the elements of every input. + | IsBelowHundred + /// No element. + | Never + + /// Tells whether an element satisfies the predicate. + member this.Matches (item : int) = + match this with + | ElementPredicate.IsEven -> item % 2 = 0 + | ElementPredicate.IsNegative -> item < 0 + | ElementPredicate.IsBelowHundred -> item < 100 + | ElementPredicate.Never -> false + +/// +/// The predicates of the search tests over the pairs of elements at the same index, as the cases of their rows. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes +/// the function of the case. +/// +[] +type PairPredicate = + /// The pairs whose sum is six. + | SumIsSix + /// The pairs whose first element is greater than the second. + | FirstIsGreater + /// The pairs whose first element is smaller than the second. + | FirstIsSmaller + /// The pairs of different elements. + | AreDifferent + /// No pair. + | Never + + /// Tells whether a pair satisfies the predicate. + member this.Matches (first : int) (second : int) = + match this with + | PairPredicate.SumIsSix -> first + second = 6 + | PairPredicate.FirstIsGreater -> first > second + | PairPredicate.FirstIsSmaller -> first < second + | PairPredicate.AreDifferent -> first <> second + | PairPredicate.Never -> false + +/// The contract of the search functions of a sequence-like module. +[] +type SeqLikeSearchContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeAccessContract<'C> (ops) + + static let never (_ : int) = false + + /// A chooser that returns a value for the negative elements only. + static let negatedIfNegative (item : int) = if item < 0 then ValueSome (-item) else ValueNone + + /// The same chooser as `negatedIfNegative`, for the FSharp.Core oracle. + static let negatedIfNegativeOption (item : int) = if item < 0 then Some (-item) else None + + /// Every input with each value to look for: the element of the singleton, two elements of the input with several, + /// both repeated, and a value that no input has. + static member InputsAndValues = seq { + for input in Inputs.All do + for value in [| 42; 5; -3; 1000 |] do + struct (input, value) + } + + /// Every input with each predicate of exists and of the searches that may find nothing. + static member InputsAndPredicates = seq { + for input in Inputs.All do + for predicate in [| ElementPredicate.IsEven; ElementPredicate.IsNegative; ElementPredicate.Never |] do + struct (input, predicate) + } + + /// Every input with each predicate of forall: one that some elements satisfy, one that every element satisfies and + /// one that no element satisfies. + static member InputsAndForallPredicates = seq { + for input in Inputs.All do + for predicate in [| ElementPredicate.IsEven; ElementPredicate.IsBelowHundred; ElementPredicate.Never |] do + struct (input, predicate) + } + + /// The predicates of find and its variants, which some elements of the input with several elements satisfy. + static member FindPredicates = seq { + ElementPredicate.IsEven + ElementPredicate.IsNegative + } + + /// The predicates of exists2. + static member Exists2Predicates = seq { + PairPredicate.SumIsSix + PairPredicate.FirstIsGreater + PairPredicate.Never + } + + /// The predicates of forall2. + static member Forall2Predicates = seq { + PairPredicate.FirstIsSmaller + PairPredicate.AreDifferent + PairPredicate.Never + } + + [.InputsAndValues)>] + member this.``contains finds an element equal to the value`` (input : int array, value : int) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.contains value elements, + ops.Contains value collection, + $"%s{ops.Name}.contains must find an element equal to the value" + ) + + [.InputsAndPredicates)>] + member this.``exists finds an element that satisfies the predicate`` (input : int array, predicate : ElementPredicate) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.exists predicate.Matches elements, + ops.Exists predicate.Matches collection, + $"%s{ops.Name}.exists must find an element that satisfies the predicate" + ) + + [.Exists2Predicates)>] + member this.``exists2 tests the pairs at the same index`` (predicate : PairPredicate) = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + + Assert.AreEqual( + Array.exists2 predicate.Matches elements otherElements, + ops.Exists2 predicate.Matches collection other, + $"%s{ops.Name}.exists2 must test the pairs at the same index" + ) + + [] + member this.``exists2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.exists2 (fun _ _ -> false) Inputs.Singleton Inputs.Several), + (fun () -> List.exists2 (fun _ _ -> false) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Exists2 (fun _ _ -> false) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "exists2 of collections of different lengths must throw" + ) + + [.FindPredicates)>] + member this.``find returns the first element that satisfies the predicate`` (predicate : ElementPredicate) = + let struct (collection, elements) = this.Create Inputs.Several + + Assert.AreEqual( + Array.find predicate.Matches elements, + ops.Find predicate.Matches collection, + $"%s{ops.Name}.find must return the first element that satisfies the predicate" + ) + + [] + member this.``find throws when no element satisfies the predicate as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.find never Inputs.Several), + (fun () -> List.find never (List.ofArray Inputs.Several)), + (fun () -> ops.Find never (ops.OfSeq Inputs.Several)), + "find without a match must throw" + ) + + [.FindPredicates)>] + member this.``findBack returns the last element that satisfies the predicate`` (predicate : ElementPredicate) = + let struct (collection, elements) = this.Create Inputs.Several + + Assert.AreEqual( + Array.findBack predicate.Matches elements, + ops.FindBack predicate.Matches collection, + $"%s{ops.Name}.findBack must return the last element that satisfies the predicate" + ) + + [] + member this.``findBack throws when no element satisfies the predicate as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.findBack never Inputs.Several), + (fun () -> List.findBack never (List.ofArray Inputs.Several)), + (fun () -> ops.FindBack never (ops.OfSeq Inputs.Several)), + "findBack without a match must throw" + ) + + [.FindPredicates)>] + member this.``findIndex returns the index of the first element that satisfies the predicate`` (predicate : ElementPredicate) = + let struct (collection, elements) = this.Create Inputs.Several + + Assert.AreEqual( + Array.findIndex predicate.Matches elements, + ops.FindIndex predicate.Matches collection, + $"%s{ops.Name}.findIndex must return the index of the first element that satisfies the predicate" + ) + + [] + member this.``findIndex throws when no element satisfies the predicate as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.findIndex never Inputs.Several), + (fun () -> List.findIndex never (List.ofArray Inputs.Several)), + (fun () -> ops.FindIndex never (ops.OfSeq Inputs.Several)), + "findIndex without a match must throw" + ) + + [.FindPredicates)>] + member this.``findIndexBack returns the index of the last element that satisfies the predicate`` + (predicate : ElementPredicate) + = + let struct (collection, elements) = this.Create Inputs.Several + + Assert.AreEqual( + Array.findIndexBack predicate.Matches elements, + ops.FindIndexBack predicate.Matches collection, + $"%s{ops.Name}.findIndexBack must return the index of the last element that satisfies the predicate" + ) + + [] + member this.``findIndexBack throws when no element satisfies the predicate as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.findIndexBack never Inputs.Several), + (fun () -> List.findIndexBack never (List.ofArray Inputs.Several)), + (fun () -> ops.FindIndexBack never (ops.OfSeq Inputs.Several)), + "findIndexBack without a match must throw" + ) + + [.InputsAndForallPredicates)>] + member this.``forall checks that every element satisfies the predicate`` (input : int array, predicate : ElementPredicate) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.forall predicate.Matches elements, + ops.Forall predicate.Matches collection, + $"%s{ops.Name}.forall must check that every element satisfies the predicate" + ) + + [.Forall2Predicates)>] + member this.``forall2 tests every pair at the same index`` (predicate : PairPredicate) = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + + Assert.AreEqual( + Array.forall2 predicate.Matches elements otherElements, + ops.Forall2 predicate.Matches collection other, + $"%s{ops.Name}.forall2 must test every pair at the same index" + ) + + [] + member this.``forall2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.forall2 (fun _ _ -> true) Inputs.Singleton Inputs.Several), + (fun () -> List.forall2 (fun _ _ -> true) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Forall2 (fun _ _ -> true) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "forall2 of collections of different lengths must throw" + ) + + [] + member this.``pick returns the first chosen value`` () = + let struct (collection, elements) = this.Create Inputs.Several + + Assert.AreEqual( + Array.pick negatedIfNegativeOption elements, + ops.Pick negatedIfNegative collection, + $"%s{ops.Name}.pick must return the first chosen value" + ) + + [] + member this.``pick throws when the chooser returns nothing as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.pick (fun _ -> None) Inputs.Several : int), + (fun () -> List.pick (fun _ -> None) (List.ofArray Inputs.Several) : int), + (fun () -> ops.Pick (fun _ -> ValueNone) (ops.OfSeq Inputs.Several)), + "pick without a choice must throw" + ) + + [.InputsAndPredicates)>] + member this.``tryFind returns the first element that satisfies the predicate or nothing`` + (input : int array, predicate : ElementPredicate) + = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryFind predicate.Matches elements + |> ValueOption.ofOption, + ops.TryFind predicate.Matches collection, + $"%s{ops.Name}.tryFind must return the first element that satisfies the predicate or nothing" + ) + + [.InputsAndPredicates)>] + member this.``tryFindBack returns the last element that satisfies the predicate or nothing`` + (input : int array, predicate : ElementPredicate) + = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryFindBack predicate.Matches elements + |> ValueOption.ofOption, + ops.TryFindBack predicate.Matches collection, + $"%s{ops.Name}.tryFindBack must return the last element that satisfies the predicate or nothing" + ) + + [.InputsAndPredicates)>] + member this.``tryFindIndex returns the index of the first element that satisfies the predicate or nothing`` + (input : int array, predicate : ElementPredicate) + = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryFindIndex predicate.Matches elements + |> ValueOption.ofOption, + ops.TryFindIndex predicate.Matches collection, + $"%s{ops.Name}.tryFindIndex must return the index of the first element that satisfies the predicate or nothing" + ) + + [.InputsAndPredicates)>] + member this.``tryFindIndexBack returns the index of the last element that satisfies the predicate or nothing`` + (input : int array, predicate : ElementPredicate) + = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryFindIndexBack predicate.Matches elements + |> ValueOption.ofOption, + ops.TryFindIndexBack predicate.Matches collection, + $"%s{ops.Name}.tryFindIndexBack must return the index of the last element that satisfies the predicate or nothing" + ) + + [)>] + member this.``tryPick returns the first chosen value or nothing`` (input : int array) = + let struct (collection, elements) = this.Create input + + Assert.AreEqual( + Array.tryPick negatedIfNegativeOption elements + |> ValueOption.ofOption, + ops.TryPick negatedIfNegative collection, + $"%s{ops.Name}.tryPick must return the first chosen value or nothing" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSortingContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSortingContract.fs new file mode 100644 index 00000000..edaf2cf5 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeSortingContract.fs @@ -0,0 +1,78 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the sorting functions of a sequence-like module. +/// +/// +/// Equal keys belong to equal elements in these inputs, so the results do not depend on the stability of the sort; +/// StableSortContract checks the stability of the modules that promise it. +/// +[] +type SeqLikeSortingContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikePartitioningContract<'C> (ops) + + [)>] + member this.``sort orders the elements ascending`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.sort elements, + ops.ToArray (ops.Sort collection), + $"%s{ops.Name}.sort must order the elements ascending" + ) + + [)>] + member this.``sortBy orders the elements by the projection`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.sortBy abs elements, + ops.ToArray (ops.SortBy abs collection), + $"%s{ops.Name}.sortBy must order the elements by the projection" + ) + + [)>] + member this.``sortByDescending orders the elements by the projection descending`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.sortByDescending abs elements, + ops.ToArray (ops.SortByDescending abs collection), + $"%s{ops.Name}.sortByDescending must order the elements by the projection descending" + ) + + [)>] + member this.``sortDescending orders the elements descending`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.sortDescending elements, + ops.ToArray (ops.SortDescending collection), + $"%s{ops.Name}.sortDescending must order the elements descending" + ) + + [)>] + member this.``sortWith orders the elements with the comparison function`` (input : int array) = + let struct (collection, elements) = this.Create input + let descending (left : int) (right : int) = compare right left + + CollectionAssert.AreEqual ( + Array.sortWith descending elements, + ops.ToArray (ops.SortWith descending collection), + $"%s{ops.Name}.sortWith must order the elements with the comparison function" + ) + + [)>] + member this.``sortWithComparer orders the elements with the comparer`` (input : int array) = + let struct (collection, elements) = this.Create input + let descending = Comparer.Create (fun left right -> compare right left) + + CollectionAssert.AreEqual ( + Array.sortDescending elements, + ops.ToArray (ops.SortWithComparer descending collection), + $"%s{ops.Name}.sortWithComparer must order the elements with the comparer" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeTransformationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeTransformationContract.fs new file mode 100644 index 00000000..d970ba11 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqLike/SeqLikeTransformationContract.fs @@ -0,0 +1,373 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The contract of the transformation functions of a sequence-like module. +[] +type SeqLikeTransformationContract<'C> (ops : SeqOps<'C>) = + inherit SeqLikeSearchContract<'C> (ops) + + static let toStructPairs (pairs : ('T1 * 'T2) array) = + pairs + |> Array.map (fun (first, second) -> struct (first, second)) + + static let toStructTriples (triples : ('T1 * 'T2 * 'T3) array) = + triples + |> Array.map (fun (first, second, third) -> struct (first, second, third)) + + /// Every input with every input, for the functions of two collections of any lengths. + static member InputPairs = seq { + for first in Inputs.All do + for second in Inputs.All do + struct (first, second) + } + + /// Every input with each size of a chunk or a window: one, inside the length, the length and beyond it. + static member InputsAndSizes = seq { + for input in Inputs.All do + for size in [| 1; 3; 8; 9 |] do + struct (input, size) + } + + [)>] + member this.``allPairs pairs every element with every element of the other collection`` (input : int array) = + let struct (collection, elements) = this.Create input + let struct (other, otherElements) = this.Create [| 1; 2; 3 |] + + CollectionAssert.AreEqual ( + Array.allPairs elements otherElements |> toStructPairs, + ops.AllPairs collection other, + $"%s{ops.Name}.allPairs must pair every element with every element of the other collection" + ) + + [.InputPairs)>] + member this.``append concatenates two collections`` (first : int array, second : int array) = + let struct (collection, elements) = this.Create first + let struct (other, otherElements) = this.Create second + + CollectionAssert.AreEqual ( + Array.append elements otherElements, + ops.ToArray (ops.Append collection other), + $"%s{ops.Name}.append must concatenate two collections" + ) + + [)>] + member this.``choose keeps the chosen values in order`` (input : int array) = + let struct (collection, elements) = this.Create input + let chooser (item : int) = if item > 0 then ValueSome (item * 10) else ValueNone + let chooserOption (item : int) = if item > 0 then Some (item * 10) else None + + CollectionAssert.AreEqual ( + Array.choose chooserOption elements, + ops.ToArray (ops.Choose chooser collection), + $"%s{ops.Name}.choose must keep the chosen values in order" + ) + + [.InputsAndSizes)>] + member this.``chunkBySize divides the elements into chunks`` (input : int array, size : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.chunkBySize size elements, + ops.ChunkBySize size collection, + $"%s{ops.Name}.chunkBySize must divide the elements into chunks" + ) + + [] + member this.``chunkBySize throws for a size that is not positive as FSharp.Core does`` (size : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.chunkBySize size Inputs.Several), + (fun () -> List.chunkBySize size (List.ofArray Inputs.Several)), + (fun () -> ops.ChunkBySize size (ops.OfSeq Inputs.Several)), + "chunkBySize of a size that is not positive must throw" + ) + + [)>] + member this.``collect concatenates the collections of the elements`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.collect (fun item -> [| item; item * 10 |]) elements, + ops.ToArray (ops.Collect (fun item -> ops.OfSeq [| item; item * 10 |]) collection), + $"%s{ops.Name}.collect must concatenate the collections of the elements" + ) + + [] + member _.``concat concatenates a sequence of collections`` () = + // A call converts an array to the sequence that OfSeq takes; the function value itself does not take an array + let collections = + Inputs.All + |> Seq.map (fun input -> ops.OfSeq input) + |> Seq.toArray + let elements = collections |> Array.map ops.ToArray + + CollectionAssert.AreEqual ( + Array.concat elements, + ops.ToArray (ops.Concat collections), + $"%s{ops.Name}.concat must concatenate the collections in order" + ) + + Assert.IsEmpty (ops.ToArray (ops.Concat Array.empty), $"%s{ops.Name}.concat of no collections must be empty") + + [)>] + member this.``filter keeps the elements that satisfy the predicate`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.filter (fun item -> item > 0) elements, + ops.ToArray (ops.Filter (fun item -> item > 0) collection), + $"%s{ops.Name}.filter must keep the elements that satisfy the predicate" + ) + + [)>] + member this.``indexed pairs every element with its index`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.indexed elements |> toStructPairs, + ops.Indexed collection, + $"%s{ops.Name}.indexed must pair every element with its index" + ) + + [)>] + member this.``map transforms every element`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.map (fun item -> item * 3) elements, + ops.ToArray (ops.Map (fun item -> item * 3) collection), + $"%s{ops.Name}.map must transform every element" + ) + + [] + member this.``map2 transforms the pairs at the same index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + + CollectionAssert.AreEqual ( + Array.map2 (fun a b -> a * 100 + b) elements otherElements, + ops.ToArray (ops.Map2 (fun a b -> a * 100 + b) collection other), + $"%s{ops.Name}.map2 must transform the pairs at the same index" + ) + + [] + member this.``map2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.map2 (+) Inputs.Singleton Inputs.Several), + (fun () -> List.map2 (+) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Map2 (+) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "map2 of collections of different lengths must throw" + ) + + [] + member this.``map3 transforms the triples at the same index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let struct (third, thirdElements) = this.Create Inputs.Third + + CollectionAssert.AreEqual ( + Array.map3 (fun a b c -> a * 10000 + b * 100 + c) elements otherElements thirdElements, + ops.ToArray (ops.Map3 (fun a b c -> a * 10000 + b * 100 + c) collection other third), + $"%s{ops.Name}.map3 must transform the triples at the same index" + ) + + [] + member this.``map3 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.map3 (fun a b c -> a + b + c) Inputs.Several Inputs.Singleton Inputs.Several), + (fun () -> + List.map3 + (fun a b c -> a + b + c) + (List.ofArray Inputs.Several) + (List.ofArray Inputs.Singleton) + (List.ofArray Inputs.Several) + ), + (fun () -> + ops.Map3 + (fun a b c -> a + b + c) + (ops.OfSeq Inputs.Several) + (ops.OfSeq Inputs.Singleton) + (ops.OfSeq Inputs.Several) + ), + "map3 of collections of different lengths must throw" + ) + + [)>] + member this.``mapi transforms every element with its index`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.mapi (fun index item -> index * 1000 + item) elements, + ops.ToArray (ops.Mapi (fun index item -> index * 1000 + item) collection), + $"%s{ops.Name}.mapi must transform every element with its index" + ) + + [] + member this.``mapi2 transforms the pairs at the same index with the index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + + CollectionAssert.AreEqual ( + Array.mapi2 (fun index a b -> index * 10000 + a * 100 + b) elements otherElements, + ops.ToArray (ops.Mapi2 (fun index a b -> index * 10000 + a * 100 + b) collection other), + $"%s{ops.Name}.mapi2 must transform the pairs at the same index with the index" + ) + + [] + member this.``mapi2 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.mapi2 (fun _ a b -> a + b) Inputs.Singleton Inputs.Several), + (fun () -> List.mapi2 (fun _ a b -> a + b) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Mapi2 (fun _ a b -> a + b) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "mapi2 of collections of different lengths must throw" + ) + + [)>] + member this.``pairwise pairs every element with its successor`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.pairwise elements |> toStructPairs, + ops.Pairwise collection, + $"%s{ops.Name}.pairwise must pair every element with its successor" + ) + + [)>] + member this.``permute moves every element to its new index`` (input : int array) = + let struct (collection, elements) = this.Create input + let indexMap index = (index + 3) % elements.Length + + CollectionAssert.AreEqual ( + Array.permute indexMap elements, + ops.ToArray (ops.Permute indexMap collection), + $"%s{ops.Name}.permute must move every element to its new index" + ) + + [] + member this.``permute throws for a function that is not a permutation as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.permute (fun _ -> 0) Inputs.Several), + (fun () -> List.permute (fun _ -> 0) (List.ofArray Inputs.Several)), + (fun () -> ops.Permute (fun _ -> 0) (ops.OfSeq Inputs.Several)), + "permute with a function that is not a permutation must throw" + ) + + [)>] + member this.``rev reverses the elements`` (input : int array) = + let struct (collection, elements) = this.Create input + CollectionAssert.AreEqual ( + Array.rev elements, + ops.ToArray (ops.Rev collection), + $"%s{ops.Name}.rev must reverse the elements" + ) + + [] + member _.``transpose turns the collections into their columns`` () = + let collections = + [| Inputs.Several; Inputs.Other; Inputs.Third |] + |> Array.map (fun input -> ops.OfSeq input) + let elements = collections |> Array.map ops.ToArray + + CollectionAssert.AreEqual ( + Array.transpose elements, + ops.Transpose collections, + $"%s{ops.Name}.transpose must turn the collections into their columns" + ) + + [] + member this.``transpose throws for collections of different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.transpose [| Inputs.Several; Inputs.Singleton |]), + (fun () -> List.transpose [| List.ofArray Inputs.Several; List.ofArray Inputs.Singleton |]), + (fun () -> ops.Transpose [| ops.OfSeq Inputs.Several; ops.OfSeq Inputs.Singleton |]), + "transpose of collections of different lengths must throw" + ) + + [] + member _.``unzip splits the pairs`` () = + let pairs = Array.zip Inputs.Several Inputs.Other + let struct (first, second) = ops.Unzip (toStructPairs pairs) + let expectedFirst, expectedSecond = Array.unzip pairs + CollectionAssert.AreEqual (expectedFirst, first, $"%s{ops.Name}.unzip must return the first components") + CollectionAssert.AreEqual (expectedSecond, second, $"%s{ops.Name}.unzip must return the second components") + + [] + member _.``unzip3 splits the triples`` () = + let triples = Array.zip3 Inputs.Several Inputs.Other Inputs.Third + let struct (first, second, third) = ops.Unzip3 (toStructTriples triples) + let expectedFirst, expectedSecond, expectedThird = Array.unzip3 triples + CollectionAssert.AreEqual (expectedFirst, first, $"%s{ops.Name}.unzip3 must return the first components") + CollectionAssert.AreEqual (expectedSecond, second, $"%s{ops.Name}.unzip3 must return the second components") + CollectionAssert.AreEqual (expectedThird, third, $"%s{ops.Name}.unzip3 must return the third components") + + [)>] + member this.``where keeps the elements that satisfy the predicate`` (input : int array) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.where (fun item -> item % 2 = 0) elements, + ops.ToArray (ops.Where (fun item -> item % 2 = 0) collection), + $"%s{ops.Name}.where must keep the elements that satisfy the predicate" + ) + + [.InputsAndSizes)>] + member this.``windowed returns the windows that slide over the elements`` (input : int array, size : int) = + let struct (collection, elements) = this.Create input + + CollectionAssert.AreEqual ( + Array.windowed size elements, + ops.Windowed size collection, + $"%s{ops.Name}.windowed must return the windows that slide over the elements" + ) + + [] + member this.``windowed throws for a size that is not positive as FSharp.Core does`` (size : int) = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.windowed size Inputs.Several), + (fun () -> List.windowed size (List.ofArray Inputs.Several)), + (fun () -> ops.Windowed size (ops.OfSeq Inputs.Several)), + "windowed of a size that is not positive must throw" + ) + + [] + member this.``zip pairs the elements at the same index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + + CollectionAssert.AreEqual ( + Array.zip elements otherElements |> toStructPairs, + ops.Zip collection other, + $"%s{ops.Name}.zip must pair the elements at the same index" + ) + + [] + member this.``zip throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.zip Inputs.Singleton Inputs.Several), + (fun () -> List.zip (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Zip (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "zip of collections of different lengths must throw" + ) + + [] + member this.``zip3 combines the elements at the same index`` () = + let struct (collection, elements) = this.Create Inputs.Several + let struct (other, otherElements) = this.Create Inputs.Other + let struct (third, thirdElements) = this.Create Inputs.Third + + CollectionAssert.AreEqual ( + Array.zip3 elements otherElements thirdElements + |> toStructTriples, + ops.Zip3 collection other third, + $"%s{ops.Name}.zip3 must combine the elements at the same index" + ) + + [] + member this.``zip3 throws for different lengths as FSharp.Core does`` () = + this.ThrowsLikeFSharpCore ( + (fun () -> Array.zip3 Inputs.Several Inputs.Singleton Inputs.Several), + (fun () -> List.zip3 (List.ofArray Inputs.Several) (List.ofArray Inputs.Singleton) (List.ofArray Inputs.Several)), + (fun () -> ops.Zip3 (ops.OfSeq Inputs.Several) (ops.OfSeq Inputs.Singleton) (ops.OfSeq Inputs.Several)), + "zip3 of collections of different lengths must throw" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqOps.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqOps.fs new file mode 100644 index 00000000..c27dc794 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SeqOps.fs @@ -0,0 +1,209 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic + +/// +/// The functions of a sequence-like module for the contract tests, over elements. +/// +/// +/// +/// A record of functions checks at compile time that a module provides every function of a facet (ADR 0001, D8). The +/// fields follow the order of the signature files. +/// +/// +/// Results of another element type are arrays: the instance of a collection converts its results with its own +/// toArray, so that one record type serves every collection. +/// +/// +type SeqOps<'C> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// The FSharp.Core module whose exceptions the module mirrors. + Mirror : Mirror + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a collection that enumerates the given elements in their order. + OfSeq : seq -> 'C + /// The elements in the order in which the collection enumerates them: the `toArray` of the module, which the + /// conversion tests check as well. + ToArray : 'C -> int array + + IsEmpty : 'C -> bool + Length : 'C -> int + + ExactlyOne : 'C -> int + Head : 'C -> int + Last : 'C -> int + Tail : 'C -> 'C + TryExactlyOne : 'C -> int voption + TryHead : 'C -> int voption + TryLast : 'C -> int voption + TryTail : 'C -> 'C voption + + Contains : int -> 'C -> bool + Exists : (int -> bool) -> 'C -> bool + Exists2 : (int -> int -> bool) -> 'C -> 'C -> bool + Find : (int -> bool) -> 'C -> int + FindBack : (int -> bool) -> 'C -> int + FindIndex : (int -> bool) -> 'C -> int + FindIndexBack : (int -> bool) -> 'C -> int + Forall : (int -> bool) -> 'C -> bool + Forall2 : (int -> int -> bool) -> 'C -> 'C -> bool + Pick : (int -> int voption) -> 'C -> int + TryFind : (int -> bool) -> 'C -> int voption + TryFindBack : (int -> bool) -> 'C -> int voption + TryFindIndex : (int -> bool) -> 'C -> int voption + TryFindIndexBack : (int -> bool) -> 'C -> int voption + TryPick : (int -> int voption) -> 'C -> int voption + + AllPairs : 'C -> 'C -> struct (int * int) array + Append : 'C -> 'C -> 'C + Choose : (int -> int voption) -> 'C -> 'C + ChunkBySize : int -> 'C -> int[][] + Collect : (int -> 'C) -> 'C -> 'C + Concat : seq<'C> -> 'C + Filter : (int -> bool) -> 'C -> 'C + Indexed : 'C -> struct (int * int) array + Map : (int -> int) -> 'C -> 'C + Map2 : (int -> int -> int) -> 'C -> 'C -> 'C + Map3 : (int -> int -> int -> int) -> 'C -> 'C -> 'C -> 'C + Mapi : (int -> int -> int) -> 'C -> 'C + Mapi2 : (int -> int -> int -> int) -> 'C -> 'C -> 'C + Pairwise : 'C -> struct (int * int) array + Permute : (int -> int) -> 'C -> 'C + Rev : 'C -> 'C + Transpose : seq<'C> -> int[][] + /// Unzips a collection of the given pairs. + Unzip : struct (int * int) array -> struct (int array * int array) + /// Unzips a collection of the given triples. + Unzip3 : struct (int * int * int) array -> struct (int array * int array * int array) + Where : (int -> bool) -> 'C -> 'C + Windowed : int -> 'C -> int[][] + Zip : 'C -> 'C -> struct (int * int) array + Zip3 : 'C -> 'C -> 'C -> struct (int * int * int) array + + /// Averages a collection of the given floats. + Average : float array -> float + AverageBy : (int -> float) -> 'C -> float + CompareWith : (int -> int -> int) -> 'C -> 'C -> int + Fold : (int array -> int -> int array) -> int array -> 'C -> int array + Fold2 : (int array -> int -> int -> int array) -> int array -> 'C -> 'C -> int array + FoldBack : (int -> int array -> int array) -> 'C -> int array -> int array + FoldBack2 : (int -> int -> int array -> int array) -> 'C -> 'C -> int array -> int array + Iter : (int -> unit) -> 'C -> unit + Iter2 : (int -> int -> unit) -> 'C -> 'C -> unit + Iteri : (int -> int -> unit) -> 'C -> unit + Iteri2 : (int -> int -> int -> unit) -> 'C -> 'C -> unit + MapFold : (int -> int -> struct (int * int)) -> int -> 'C -> struct (int array * int) + MapFoldBack : (int -> int -> struct (int * int)) -> 'C -> int -> struct (int array * int) + Max : 'C -> int + MaxBy : (int -> int) -> 'C -> int + Min : 'C -> int + MinBy : (int -> int) -> 'C -> int + Reduce : (int -> int -> int) -> 'C -> int + ReduceBack : (int -> int -> int) -> 'C -> int + Scan : (int array -> int -> int array) -> int array -> 'C -> int[][] + ScanBack : (int -> int array -> int array) -> 'C -> int array -> int[][] + Sum : 'C -> int + SumBy : (int -> int) -> 'C -> int + + CountBy : (int -> int) -> 'C -> struct (int * int) array + Distinct : 'C -> 'C + DistinctBy : (int -> int) -> 'C -> 'C + Except : seq -> 'C -> 'C + GroupBy : (int -> int) -> 'C -> struct (int * int array) array + Partition : (int -> bool) -> 'C -> struct ('C * 'C) + PartitionWith : (int -> Choice) -> 'C -> struct (int array * string array) + Skip : int -> 'C -> 'C + SkipLast : int -> 'C -> 'C + SkipWhile : (int -> bool) -> 'C -> 'C + SplitAt : int -> 'C -> struct ('C * 'C) + SplitInto : int -> 'C -> int[][] + Take : int -> 'C -> 'C + TakeLast : int -> 'C -> 'C + TakeWhile : (int -> bool) -> 'C -> 'C + Truncate : int -> 'C -> 'C + + Sort : 'C -> 'C + SortBy : (int -> int) -> 'C -> 'C + SortByDescending : (int -> int) -> 'C -> 'C + SortDescending : 'C -> 'C + SortWith : (int -> int -> int) -> 'C -> 'C + SortWithComparer : IComparer -> 'C -> 'C + + RandomChoice : 'C -> int + RandomChoiceBy : (unit -> float) -> 'C -> int + RandomChoiceWith : Random -> 'C -> int + RandomChoices : int -> 'C -> 'C + RandomChoicesBy : (unit -> float) -> int -> 'C -> 'C + RandomChoicesWith : Random -> int -> 'C -> 'C + RandomSample : int -> 'C -> 'C + RandomSampleBy : (unit -> float) -> int -> 'C -> 'C + RandomSampleWith : Random -> int -> 'C -> 'C + RandomShuffle : 'C -> 'C + RandomShuffleBy : (unit -> float) -> 'C -> 'C + RandomShuffleWith : Random -> 'C -> 'C + + ToList : 'C -> int list + ToSeq : 'C -> seq +} + +/// +/// The creation functions of a sequence-like module for the contract tests. +/// +/// +/// A function value cannot take a span, so the span functions take an array that the instance turns into a span. +/// +type SeqCreationOps<'C> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// The FSharp.Core module whose exceptions the module mirrors. + Mirror : Mirror + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// The elements in the order in which the collection enumerates them. + ToArray : 'C -> int array + + Empty : unit -> 'C + Init : int -> (int -> int) -> 'C + OfArray : int array -> 'C + OfList : int list -> 'C + OfReadOnlySpan : int array -> 'C + OfSeq : seq -> 'C + OfSpan : int array -> 'C + Replicate : int -> int -> 'C + Singleton : int -> 'C + Unfold : (int -> struct (int * int) voption) -> int -> 'C +} + +/// +/// The index-based functions of a sequence-like module for the contract tests. +/// +type IndexedOps<'C> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// The FSharp.Core module whose exceptions the module mirrors. + Mirror : Mirror + /// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. + ParamName : string -> string + /// Creates a collection that enumerates the given elements in their order. + OfSeq : seq -> 'C + /// The elements in the order in which the collection enumerates them. + ToArray : 'C -> int array + + Item : int -> 'C -> int + TryItem : int -> 'C -> int voption + + IndexOf : int -> 'C -> int + IndexOfWith : IEqualityComparer -> int -> 'C -> int + LastIndexOf : int -> 'C -> int + LastIndexOfWith : IEqualityComparer -> int -> 'C -> int + + Add : int -> 'C -> 'C + InsertAt : int -> int -> 'C -> 'C + InsertManyAt : int -> seq -> 'C -> 'C + RemoveAt : int -> 'C -> 'C + RemoveManyAt : int -> int -> 'C -> 'C + UpdateAt : int -> int -> 'C -> 'C +} diff --git a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj index 547aa7af..67aef272 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -22,7 +22,29 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListAssert.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListAssert.fs new file mode 100644 index 00000000..414447a4 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListAssert.fs @@ -0,0 +1,15 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The assertions of the tests of the flat list and of its builder. +[] +type FlatListAssert = + + /// Asserts that an action rejects its input with an ArgumentNullException that names the parameter. + static member RejectsNull (label : string, paramName : string, actual : unit -> unit) = + let ex = + Assert.ThrowsExactly(Action actual, $"%s{label} must throw ArgumentNullException") + + Assert.AreEqual(paramName, ex.ParamName, $"%s{label} must name the parameter of the rejected input") diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListBuilderTests.fs new file mode 100644 index 00000000..b9e80eb2 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListBuilderTests.fs @@ -0,0 +1,767 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The builder functions of `FlatList` and the `FlatListBuilder` module. +[] +type FlatListBuilderTests () = + + /// A builder that holds the elements of the input with several elements. + static let filled () = + let builder = FlatList.builder() + builder.AddRange Inputs.Several + builder + + + /// The length of the arrays that the blits copy into. + static let targetLength = 5 + + /// The functions of the builder that take a sequence, by name and by the input that they reject, with the parameter + /// that names the input and the function on a builder. + static let sequenceRejections : Map unit))> = + let nullValues = Unchecked.defaultof> + let defaultValues = Unchecked.defaultof> :> seq + let integers () = FlatList.builder() + let objects () = FlatList.builder() + let comparer = EqualityComparer.Default + + Map.ofArray [| + "addRange (default flat list of strings as objects)", + struct ("values", + (fun () -> + FlatListBuilder.addRange FlatListOracles.defaultStringsAsObjects (objects ()) + |> ignore + )) + "insertManyAt (null sequence)", + struct ("values", + (fun () -> + FlatListBuilder.insertManyAt 0 nullValues (integers ()) + |> ignore + )) + "insertManyAt (default flat list)", + struct ("values", + (fun () -> + FlatListBuilder.insertManyAt 0 defaultValues (integers ()) + |> ignore + )) + "insertManyAt (default flat list of strings as objects)", + struct ("values", + (fun () -> + FlatListBuilder.insertManyAt 0 FlatListOracles.defaultStringsAsObjects (objects ()) + |> ignore + )) + "removeMany (null sequence)", + struct ("items", + (fun () -> + FlatListBuilder.removeMany nullValues (integers ()) + |> ignore + )) + "removeMany (default flat list)", + struct ("items", + (fun () -> + FlatListBuilder.removeMany defaultValues (integers ()) + |> ignore + )) + "removeMany (default flat list of strings as objects)", + struct ("items", + (fun () -> + FlatListBuilder.removeMany FlatListOracles.defaultStringsAsObjects (objects ()) + |> ignore + )) + "removeManyWith (null sequence)", + struct ("items", + (fun () -> + FlatListBuilder.removeManyWith comparer nullValues (integers ()) + |> ignore + )) + "removeManyWith (default flat list)", + struct ("items", + (fun () -> + FlatListBuilder.removeManyWith comparer defaultValues (integers ()) + |> ignore + )) + "removeManyWith (default flat list of strings as objects)", + struct ("items", + (fun () -> + FlatListBuilder.removeManyWith + EqualityComparer.Default + FlatListOracles.defaultStringsAsObjects + (objects ()) + |> ignore + )) + |] + + /// The functions of the builder that change it and return it, by name, applied with valid arguments to a builder of + /// the input with several elements. + static let chainedFunctions : Map.Builder -> FlatList.Builder> = + let comparer = EqualityComparer.Default + + Map.ofArray [| + nameof FlatListBuilder.insertManyAt, FlatListBuilder.insertManyAt 1 [| 100 |] + nameof FlatListBuilder.removeAll, FlatListBuilder.removeAll (fun item -> item < 0) + nameof FlatListBuilder.removeMany, FlatListBuilder.removeMany [| 5 |] + nameof FlatListBuilder.removeManyAt, FlatListBuilder.removeManyAt 1 2 + nameof FlatListBuilder.removeManyWith, FlatListBuilder.removeManyWith comparer [| 5 |] + nameof FlatListBuilder.replace, FlatListBuilder.replace 5 99 + nameof FlatListBuilder.replaceWith, FlatListBuilder.replaceWith comparer 5 99 + nameof FlatListBuilder.sortRange, FlatListBuilder.sortRange 1 4 + nameof FlatListBuilder.sortRangeWith, FlatListBuilder.sortRangeWith compare 1 4 + nameof FlatListBuilder.sortRangeWithComparer, FlatListBuilder.sortRangeWithComparer Comparer.Default 1 4 + |] + + /// The functions that sort a range, by name, with the function and the sort of the range in the oracle. The names are + /// the ones of the table of FlatList, so that the rows of its tests run here too. + static let rangeSorts + : Map int -> FlatList.Builder -> FlatList.Builder) * (int array -> int array))> = + Map.ofArray [| + nameof FlatList.sortRange, struct (FlatListBuilder.sortRange, Array.sortWith compare) + nameof FlatList.sortRangeWith, + struct (FlatListBuilder.sortRangeWith FlatListOracles.descending, Array.sortWith FlatListOracles.descending) + nameof FlatList.sortRangeWithComparer, + struct (FlatListBuilder.sortRangeWithComparer (Comparer.Create(Comparison FlatListOracles.byAbsolute)), + Array.sortWith FlatListOracles.byAbsolute) + |] + + /// The predicates of removeAll, by name. + static let removalPredicates : Map bool> = + Map.ofArray [| + "negative", (fun item -> item < 0) + "positive", (fun item -> item > 0) + "every element", (fun _ -> true) + "no element", (fun _ -> false) + |] + + /// The names of the functions that take a sequence, as the rows of the test of the sequences that they reject. + static member SequenceRejections = sequenceRejections.Keys + + /// The names of the functions that change the builder and return it, as the rows of their test. + static member ChainedFunctions = chainedFunctions.Keys + + /// Every index of every input, with the values to insert: none, one and two. + static member InsertionPoints = seq { + for input in Inputs.All do + for index in 0 .. input.Length do + for values in [| [||]; [| 100 |]; [| 100; 200 |] |] do + struct (input, index, values) + } + + /// Every input with an index below its first element and one beyond its end. + static member InsertionPointsOutOfRange = seq { + for input in Inputs.All do + for index in [| -1; input.Length + 1 |] do + struct (input, index) + } + + /// Every range of every input, as the index of its first element and its length. + static member RemovalRanges = seq { + for input in Inputs.All do + for index in 0 .. input.Length do + for count in 0 .. input.Length - index do + struct (input, index, count) + } + + /// The ranges that the removal of a range rejects: an index out of range, and a count that is negative or beyond the + /// end. + static member RemovalRangesOutOfRange = seq { + struct (Inputs.Empty, 1, 0) + struct (Inputs.Empty, 0, 1) + struct (Inputs.Empty, -1, 0) + struct (Inputs.Several, -1, 0) + struct (Inputs.Several, 9, 0) + struct (Inputs.Several, 0, -1) + struct (Inputs.Several, 0, 9) + struct (Inputs.Several, 5, 4) + struct (Inputs.Several, 8, 1) + struct (Inputs.Several, -1, -1) + } + + /// Every predicate of removeAll on every input. + static member RemovalPredicates = seq { + for name in removalPredicates.Keys do + for input in Inputs.All do + struct (name, input) + } + + /// Every range of every input that fits a target of five elements, copied to the first and to the last place that fits. + static member BlitRanges = seq { + for input in Inputs.All do + for sourceIndex in 0 .. input.Length do + for count in 0 .. min (input.Length - sourceIndex) targetLength do + for targetIndex in Array.distinct [| 0; targetLength - count |] do + struct (input, sourceIndex, targetIndex, count) + } + + /// The ranges that the blit rejects, for a target of five elements: in the order of the checks of the BCL, a negative + /// count, a source range out of range and a target range out of range. + static member BlitRangesOutOfRange = seq { + struct (Inputs.Empty, 1, 0, 0) + struct (Inputs.Empty, 0, 0, 1) + struct (Inputs.Several, 6, 0, 3) + struct (Inputs.Several, 0, 3, 3) + struct (Inputs.Several, 0, 6, 0) + struct (Inputs.Several, -1, 0, 1) + struct (Inputs.Several, 0, -1, 1) + struct (Inputs.Several, 0, 0, -1) + struct (Inputs.Several, 9, 0, 0) + struct (Inputs.Several, 0, 0, 9) + struct (Inputs.Several, -1, -1, -1) + } + + [] + member _.``build returns the elements that the action adds`` () = + let array = FlatList.build (fun builder -> builder.AddRange Inputs.Several) + CollectionAssert.AreEqual (Inputs.Several, FlatList.toArray array, "FlatList.build must return the added elements") + + [] + member _.``addRange rejects a default flat list, which it takes as a sequence`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FlatListBuilder.addRange (Unchecked.defaultof> :> seq<_>) (FlatList.builder()) + |> ignore + ), + "FlatListBuilder.addRange must reject a default flat list" + ) + + Assert.AreEqual( + "values", + ex.ParamName, + "FlatListBuilder.addRange must name the parameter of the default flat list" + ) + + [] + member _.``addRange rejects a null sequence and names the parameter of the function`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FlatListBuilder.addRange Unchecked.defaultof> (FlatList.builder()) + |> ignore + ), + "FlatListBuilder.addRange must reject a null sequence" + ) + + Assert.AreEqual( + "values", + ex.ParamName, + "FlatListBuilder.addRange must name its own parameter, not the one of the BCL" + ) + + [] + member _.``builder starts empty`` () = + let builder = FlatList.builder() + Assert.AreEqual(0, builder.Count, "FlatList.builder must start without elements") + + [] + member _.``builderWith reserves the capacity`` () = + let builder = FlatList.builderWith 10 + Assert.AreEqual(10, builder.Capacity, "FlatList.builderWith must reserve the capacity") + Assert.AreEqual(0, builder.Count, "FlatList.builderWith must start without elements") + + [] + member _.``builderWith throws for a negative capacity`` () = + Assert.ThrowsExactly( + Action (fun () -> FlatList.builderWith -1 |> ignore), + "FlatList.builderWith must reject a negative capacity as the BCL does" + ) + |> ignore + + [] + member _.``moveFromBuilder takes the contents of a full builder`` () = + let builder = FlatList.builderWith 3 + builder.AddRange [| 1; 2; 3 |] + let array = FlatList.moveFromBuilder builder + CollectionAssert.AreEqual ([| 1; 2; 3 |], FlatList.toArray array, "FlatList.moveFromBuilder must return the elements") + Assert.AreEqual(0, builder.Count, "FlatList.moveFromBuilder must empty the builder") + + [] + member _.``moveFromBuilder throws for a builder that is not full`` () = + let builder = FlatList.builderWith 3 + builder.Add 1 + + Assert.ThrowsExactly( + Action (fun () -> FlatList.moveFromBuilder builder |> ignore), + "FlatList.moveFromBuilder must reject a builder whose count differs from its capacity" + ) + |> ignore + + [] + member _.``ofBuilder copies the contents and keeps the builder`` () = + let builder = filled () + let array = FlatList.ofBuilder builder + builder.Add 99 + CollectionAssert.AreEqual (Inputs.Several, FlatList.toArray array, "FlatList.ofBuilder must copy the elements") + Assert.AreEqual(Inputs.Several.Length + 1, builder.Count, "FlatList.ofBuilder must keep the builder usable") + + [] + member _.``ofBuilder throws for a null builder`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + FlatList.ofBuilder (Unchecked.defaultof.Builder>) + |> ignore + ), + "FlatList.ofBuilder must reject a null builder" + ) + + Assert.AreEqual("builder", ex.ParamName, "FlatList.ofBuilder must name the builder parameter") + + [] + member _.``toBuilder copies the elements into a new builder`` () = + let array = FlatList.ofArray Inputs.Several + let builder = FlatList.toBuilder array + builder.Add 99 + + CollectionAssert.AreEqual ( + Array.append Inputs.Several [| 99 |], + Seq.toArray builder, + "FlatList.toBuilder must hold the elements" + ) + + CollectionAssert.AreEqual (Inputs.Several, FlatList.toArray array, "FlatList.toBuilder must not change the flat list") + + [] + member _.``update applies the action to a copy`` () = + let array = FlatList.ofArray Inputs.Several + let updated = FlatList.update (fun builder -> builder.RemoveAt 0) array + CollectionAssert.AreEqual (Array.tail Inputs.Several, FlatList.toArray updated, "FlatList.update must apply the action") + CollectionAssert.AreEqual (Inputs.Several, FlatList.toArray array, "FlatList.update must not change the input") + + [] + member _.``add, addRange and insertAt return the same builder`` () = + let builder = FlatList.builder() + let result = + builder + |> FlatListBuilder.add 1 + |> FlatListBuilder.addRange [ 2; 3 ] + |> FlatListBuilder.insertAt 0 0 + Assert.AreSame (builder, result, "The functions that change a builder must return it") + CollectionAssert.AreEqual ([| 0; 1; 2; 3 |], Seq.toArray builder, "add, addRange and insertAt must change the builder") + + [] + member _.``capacity, setCapacity and length report the builder`` () = + let builder = + FlatList.builderWith 4 + |> FlatListBuilder.add 1 + |> FlatListBuilder.setCapacity 10 + Assert.AreEqual(10, FlatListBuilder.capacity builder, "FlatListBuilder.capacity must return the capacity") + Assert.AreEqual(1, FlatListBuilder.length builder, "FlatListBuilder.length must count the elements") + + [] + member _.``clear removes every element`` () = + let builder = filled () |> FlatListBuilder.clear + Assert.AreEqual(0, FlatListBuilder.length builder, "FlatListBuilder.clear must remove every element") + + [] + member _.``contains, indexOf and lastIndexOf search the builder`` () = + let builder = filled () + Assert.IsTrue (FlatListBuilder.contains 8 builder, "FlatListBuilder.contains must find 8") + Assert.AreEqual(1, FlatListBuilder.indexOf -3 builder, "FlatListBuilder.indexOf must return the first index") + Assert.AreEqual(5, FlatListBuilder.lastIndexOf -3 builder, "FlatListBuilder.lastIndexOf must return the last index") + Assert.AreEqual(-1, FlatListBuilder.indexOf 1000 builder, "FlatListBuilder.indexOf must return -1 when absent") + + [] + member _.``drainToFlatList returns the elements and empties the builder`` () = + let builder = filled () + let array = FlatListBuilder.drainToFlatList builder + + CollectionAssert.AreEqual ( + Inputs.Several, + FlatList.toArray array, + "FlatListBuilder.drainToFlatList must return the elements" + ) + + Assert.AreEqual(0, FlatListBuilder.length builder, "FlatListBuilder.drainToFlatList must empty the builder") + + [] + member _.``item and updateAt read and replace an element`` () = + let builder = filled () |> FlatListBuilder.updateAt 2 99 + Assert.AreEqual(99, FlatListBuilder.item 2 builder, "FlatListBuilder.updateAt must replace the element") + + [] + member _.``remove and removeAt remove elements`` () = + let builder = filled () + Assert.IsTrue (FlatListBuilder.remove -3 builder, "FlatListBuilder.remove must report a removed element") + Assert.IsFalse (FlatListBuilder.remove 1000 builder, "FlatListBuilder.remove must report a missing element") + FlatListBuilder.removeAt 0 builder |> ignore + CollectionAssert.AreEqual ([| 8; 5; 0; -3; 12; 7 |], Seq.toArray builder, "remove and removeAt must remove the elements") + + [] + member _.``rev reverses the builder`` () = + let builder = filled () |> FlatListBuilder.rev + CollectionAssert.AreEqual (Array.rev Inputs.Several, Seq.toArray builder, "FlatListBuilder.rev must reverse the elements") + + [] + member _.``sort, sortWith and sortWithComparer order the builder`` () = + CollectionAssert.AreEqual ( + Array.sort Inputs.Several, + Seq.toArray (filled () |> FlatListBuilder.sort), + "FlatListBuilder.sort must sort ascending" + ) + + CollectionAssert.AreEqual ( + Array.sortDescending Inputs.Several, + Seq.toArray ( + filled () + |> FlatListBuilder.sortWith (fun left right -> compare right left) + ), + "FlatListBuilder.sortWith must use the comparison function" + ) + + CollectionAssert.AreEqual ( + Array.sortDescending Inputs.Several, + Seq.toArray ( + filled () + |> FlatListBuilder.sortWithComparer (Comparer.Create (fun left right -> compare right left)) + ), + "FlatListBuilder.sortWithComparer must use the comparer" + ) + + [] + member _.``toArray copies the elements`` () = + let builder = filled () + CollectionAssert.AreEqual ( + Inputs.Several, + FlatListBuilder.toArray builder, + "FlatListBuilder.toArray must copy the elements" + ) + + [] + member _.``functions that change the builder return it`` (name : string) = + let builder = filled () + let result = chainedFunctions[name] builder + Assert.AreSame (builder, result, $"FlatListBuilder.%s{name} must return the builder that it changed") + + [] + member _.``functions that take a sequence reject a null sequence, a default flat list and a default flat list of strings`` + (name : string) + = + let struct (paramName, actual) = sequenceRejections[name] + FlatListAssert.RejectsNull ($"FlatListBuilder.%s{name}", paramName, actual) + + [] + member _.``a flat list of strings that is not default passes as a sequence of objects`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + let builder = FlatList.builder() + + // Add the elements, insert them again after the first one, then remove one element for each of them + builder + |> FlatListBuilder.addRange widened + |> FlatListBuilder.insertManyAt 1 widened + |> FlatListBuilder.removeMany widened + |> ignore + + CollectionAssert.AreEqual ( + [| "a" :> obj; "b" :> obj |], + Seq.toArray builder, + "addRange, insertManyAt and removeMany must take the elements of a flat list of strings as objects" + ) + [] + member _.``insertManyAt inserts the values at the index as Array.insertManyAt does`` + (input : int array, index : int, values : int array) + = + let builder = FlatListOracles.builderOf input + FlatListBuilder.insertManyAt index values builder |> ignore + + CollectionAssert.AreEqual ( + Array.insertManyAt index values input, + Seq.toArray builder, + "FlatListBuilder.insertManyAt must insert the values at the index" + ) + + [] + member _.``insertManyAt throws for an index out of range as the BCL does`` (input : int array, index : int) = + ExceptionAssert.Same ( + id, + (fun () -> (FlatListOracles.builderOf input).InsertRange(index, [| 100 |])), + (fun () -> + FlatListBuilder.insertManyAt index [| 100 |] (FlatListOracles.builderOf input) + |> ignore + ), + "FlatListBuilder.insertManyAt must throw as InsertRange does for an index out of range" + ) + + [] + member _.``insertManyAt checks the index before the values as the BCL does`` (index : int) = + let values = Unchecked.defaultof> + + ExceptionAssert.Same ( + id, + (fun () -> (filled ()).InsertRange(index, values)), + (fun () -> + FlatListBuilder.insertManyAt index values (filled ()) + |> ignore + ), + "FlatListBuilder.insertManyAt must report the index out of range, not the missing values" + ) + + [] + member _.``removeManyAt removes the range as Array.removeManyAt does`` (input : int array, index : int, count : int) = + let builder = FlatListOracles.builderOf input + FlatListBuilder.removeManyAt index count builder |> ignore + + CollectionAssert.AreEqual ( + Array.removeManyAt index count input, + Seq.toArray builder, + "FlatListBuilder.removeManyAt must remove the range" + ) + + [] + member _.``removeManyAt throws for a range out of range as the BCL does`` (input : int array, index : int, count : int) = + // RemoveRange calls the count length, and the function calls it count + let countOfLength (parameter : string) = + match parameter with + | "length" -> "count" + | other -> other + + ExceptionAssert.Same ( + countOfLength, + (fun () -> (FlatListOracles.builderOf input).RemoveRange(index, count)), + (fun () -> + FlatListBuilder.removeManyAt index count (FlatListOracles.builderOf input) + |> ignore + ), + "FlatListBuilder.removeManyAt must throw as RemoveRange does, with the parameter name count" + ) + + [)>] + member _.``removeMany removes one element for each value`` (input : int array, items : int array) = + let builder = FlatListOracles.builderOf input + FlatListBuilder.removeMany items builder |> ignore + + CollectionAssert.AreEqual ( + Array.fold (fun elements item -> FlatListOracles.removeFirst (=) item elements) input items, + Seq.toArray builder, + "FlatListBuilder.removeMany must remove one element for each value" + ) + + [] + member _.``removeMany compares in the default equality, which tells b from B`` () = + let builder = FlatListOracles.builderOf (FlatListOracles.names ()) + FlatListBuilder.removeMany [| "B" |] builder |> ignore + CollectionAssert.AreEqual ( + [| "a"; "b"; "c" |], + Seq.toArray builder, + "FlatListBuilder.removeMany must not find B equal to b" + ) + + [)>] + member _.``removeManyWith removes one element for each value that the comparer finds equal`` (items : string array) = + let builder = FlatListOracles.builderOf (FlatListOracles.names ()) + FlatListBuilder.removeManyWith StringComparer.OrdinalIgnoreCase items builder + |> ignore + + CollectionAssert.AreEqual ( + Array.fold + (fun elements item -> FlatListOracles.removeFirst FlatListOracles.equalIgnoringCase item elements) + (FlatListOracles.names ()) + items, + Seq.toArray builder, + "FlatListBuilder.removeManyWith must remove one element for each value that the comparer finds equal" + ) + + [)>] + member _.``removeWith removes the first element that the comparer finds equal and reports it`` (value : string) = + let names = FlatListOracles.names () + let builder = FlatListOracles.builderOf names + let removed = FlatListBuilder.removeWith StringComparer.OrdinalIgnoreCase value builder + let expected = FlatListOracles.removeFirst FlatListOracles.equalIgnoringCase value names + + Assert.AreEqual( + expected.Length < names.Length, + removed, + "FlatListBuilder.removeWith must report whether it removed an element" + ) + + CollectionAssert.AreEqual ( + expected, + Seq.toArray builder, + "FlatListBuilder.removeWith must remove the first element that the comparer finds equal" + ) + + [] + member _.``removeAll removes the elements that satisfy the predicate`` (name : string, input : int array) = + let predicate = removalPredicates[name] + let builder = FlatListOracles.builderOf input + FlatListBuilder.removeAll predicate builder |> ignore + + CollectionAssert.AreEqual ( + input |> Array.filter (predicate >> not), + Seq.toArray builder, + $"FlatListBuilder.removeAll must remove the elements of the predicate: %s{name}" + ) + + [)>] + member _.``replace replaces the first equal element`` (input : int array, oldValue : int) = + let builder = FlatListOracles.builderOf input + FlatListBuilder.replace oldValue 99 builder |> ignore + + CollectionAssert.AreEqual ( + FlatListOracles.replaceFirst (=) oldValue 99 input + |> ValueOption.get, + Seq.toArray builder, + "FlatListBuilder.replace must replace the first equal element" + ) + + [)>] + member _.``replace leaves the builder as it is when no element equals the value, as the BCL does`` + (input : int array, value : int) + = + let builder = FlatListOracles.builderOf input + FlatListBuilder.replace value 0 builder |> ignore + + CollectionAssert.AreEqual ( + input, + Seq.toArray builder, + "FlatListBuilder.replace must not change the builder and must not throw when no element equals the value" + ) + + [)>] + member _.``replaceWith replaces the first element that the comparer finds equal`` (oldValue : string) = + let names = FlatListOracles.names () + let builder = FlatListOracles.builderOf names + FlatListBuilder.replaceWith StringComparer.OrdinalIgnoreCase oldValue "X" builder + |> ignore + + CollectionAssert.AreEqual ( + FlatListOracles.replaceFirst FlatListOracles.equalIgnoringCase oldValue "X" names + |> ValueOption.get, + Seq.toArray builder, + "FlatListBuilder.replaceWith must replace the first element that the comparer finds equal" + ) + + [] + member _.``replaceWith leaves the builder as it is when no element equals the value, as the BCL does`` () = + let names = FlatListOracles.names () + let builder = FlatListOracles.builderOf names + FlatListBuilder.replaceWith StringComparer.OrdinalIgnoreCase "d" "X" builder + |> ignore + + CollectionAssert.AreEqual ( + names, + Seq.toArray builder, + "FlatListBuilder.replaceWith must not change the builder and must not throw when no element equals the value" + ) + + [] + member _.``replace and replaceWith compare in the default equality, which tells b from B, for a null comparer`` () = + let comparer = Unchecked.defaultof> + + let replaced = + FlatListOracles.builderOf (FlatListOracles.names ()) + |> FlatListBuilder.replaceWith comparer "B" "X" + + let plain = + FlatListOracles.builderOf (FlatListOracles.names ()) + |> FlatListBuilder.replace "B" "X" + + CollectionAssert.AreEqual ( + [| "a"; "b"; "X"; "c" |], + Seq.toArray replaced, + "FlatListBuilder.replaceWith must compare in the default equality for a null comparer" + ) + + CollectionAssert.AreEqual ( + [| "a"; "b"; "X"; "c" |], + Seq.toArray plain, + "FlatListBuilder.replace must not find B equal to b" + ) + + [)>] + member _.``functions that sort a range sort the range and keep the other elements`` + (name : string, input : int array, index : int, count : int) + = + let struct (sortRange, sortPart) = rangeSorts[name] + let builder = FlatListOracles.builderOf input + sortRange index count builder |> ignore + + let expected = + Array.concat [| + Array.sub input 0 index + sortPart (Array.sub input index count) + Array.sub input (index + count) (input.Length - index - count) + |] + + CollectionAssert.AreEqual ( + expected, + Seq.toArray builder, + $"FlatListBuilder.%s{name} must sort the range and keep the other elements" + ) + + [)>] + member _.``functions that sort a range throw for a range out of range as the BCL does`` + (name : string, index : int, count : int, paramName : string) + = + let struct (sortRange, _) = rangeSorts[name] + + let ex = + Assert.ThrowsExactly( + Action (fun () -> sortRange index count (filled ()) |> ignore), + $"FlatListBuilder.%s{name} must throw ArgumentOutOfRangeException for a range out of range" + ) + + Assert.AreEqual( + paramName, + ex.ParamName, + $"FlatListBuilder.%s{name} must name the parameter that the BCL names" + ) + + [] + member _.``sortRange orders strings ordinally, as F# generic comparison does`` () = + let builder = FlatListOracles.builderOf [| "b"; "A"; "a"; "B" |] + FlatListBuilder.sortRange 0 4 builder |> ignore + + CollectionAssert.AreEqual ( + [| "A"; "B"; "a"; "b" |], + Seq.toArray builder, + "FlatListBuilder.sortRange must sort strings ordinally" + ) + + [] + member _.``blit copies the range into the array as Array.blit does`` + (input : int array, sourceIndex : int, targetIndex : int, count : int) + = + let target = Array.create targetLength (-1) + let expected = Array.copy target + Array.blit input sourceIndex expected targetIndex count + FlatListBuilder.blit (FlatListOracles.builderOf input) sourceIndex target targetIndex count + + CollectionAssert.AreEqual (expected, target, "FlatListBuilder.blit must copy the range into the array") + + [] + member _.``blit throws for a range out of range as the BCL does`` + (input : int array, sourceIndex : int, targetIndex : int, count : int) + = + // CopyTo calls the target destination, its index destinationIndex and the count length + let ofBcl (parameter : string) = + match parameter with + | "destination" -> "target" + | "destinationIndex" -> "targetIndex" + | "length" -> "count" + | other -> other + + ExceptionAssert.Same ( + ofBcl, + (fun () -> (FlatListOracles.builderOf input).CopyTo(sourceIndex, Array.zeroCreate targetLength, targetIndex, count)), + (fun () -> + FlatListBuilder.blit + (FlatListOracles.builderOf input) + sourceIndex + (Array.zeroCreate targetLength) + targetIndex + count + ), + "FlatListBuilder.blit must throw as CopyTo does, with the parameter names of the function" + ) + + [] + member _.``blit rejects a null array before the ranges as the BCL does`` (sourceIndex : int, targetIndex : int, count : int) = + let target = Unchecked.defaultof + + FlatListAssert.RejectsNull ( + "FlatListBuilder.blit", + "target", + (fun () -> FlatListBuilder.blit (filled ()) sourceIndex target targetIndex count) + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListComputationExpressionTests.fs new file mode 100644 index 00000000..93e6248f --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListComputationExpressionTests.fs @@ -0,0 +1,243 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `flatList { }` computation expression. +[] +type FlatListComputationExpressionTests () = + + static let defaultArray = Unchecked.defaultof> + + + [] + member _.``an expression without elements is empty`` () = + let array : FlatList = flatList { () } + Assert.IsTrue (FlatList.isEmpty array, "flatList { () } must be empty") + + [] + member _.``yield adds the elements in order`` () = + let array = flatList { + yield 1 + yield 2 + yield 3 + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], FlatList.toArray array, "flatList must keep the order of yield") + + [] + member _.``implicit yields add the elements in order`` () = + let array = flatList { + 1 + 2 + 3 + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], FlatList.toArray array, "flatList must keep the order of implicit yields") + + [] + member _.``yield! adds a sequence, an array, a flat list and another expression`` () = + let inner = flatList { yield 7 } + + let array = flatList { + yield! [ 1; 2 ] + yield! [| 3; 4 |] + yield! FlatList.ofArray [| 5; 6 |] + yield! inner + } + + CollectionAssert.AreEqual ([| 1; 2; 3; 4; 5; 6; 7 |], FlatList.toArray array, "flatList must add the elements of yield!") + + [] + member _.``yield! rejects a default flat list as the functions of FlatList do`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> flatList { yield! defaultArray } |> ignore), + "flatList must reject a default flat list in yield!" + ) + + Assert.AreEqual("items", ex.ParamName, "flatList must name the parameter of the default flat list") + + [] + member _.``yield! rejects a default flat list given as a sequence as FlatList.ofSeq does`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> flatList { yield! (defaultArray :> seq) } |> ignore), + "flatList must reject a default flat list in yield! of a sequence" + ) + + Assert.AreEqual("items", ex.ParamName, "flatList must name the parameter of the default flat list") + + [] + member _.``yield! rejects a null sequence`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> flatList { yield! Unchecked.defaultof> } |> ignore), + "flatList must reject a null sequence in yield!" + ) + + Assert.AreEqual("items", ex.ParamName, "flatList must name the parameter of the null sequence") + + [] + member _.``yield! rejects a default flat list of strings given as a sequence of objects`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + flatList { yield! FlatListOracles.defaultStringsAsObjects } + |> ignore + ), + "flatList must reject a default flat list of strings in yield! of a sequence of objects" + ) + + Assert.AreEqual("items", ex.ParamName, "flatList must name the parameter of the default flat list") + + [] + member _.``for adds the elements of the body for each item`` () = + let array = flatList { + for item in 1..3 do + yield item * 10 + } + + CollectionAssert.AreEqual ([| 10; 20; 30 |], FlatList.toArray array, "flatList must run the body of for for each item") + + [] + member _.``for rejects a default flat list, which it takes as a sequence`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + flatList { + for item in defaultArray do + yield item + } + |> ignore + ), + "flatList must reject a default flat list in for" + ) + + Assert.AreEqual("source", ex.ParamName, "flatList must name the parameter of the default flat list") + + [] + member _.``for rejects a null sequence`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + flatList { + for item in Unchecked.defaultof> do + yield item + } + |> ignore + ), + "flatList must reject a null sequence in for" + ) + + Assert.AreEqual("source", ex.ParamName, "flatList must name the parameter of the null sequence") + + [] + member _.``for rejects a default flat list of strings given as a sequence of objects`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + flatList { + for item in FlatListOracles.defaultStringsAsObjects do + yield item + } + |> ignore + ), + "flatList must reject a default flat list of strings in for over a sequence of objects" + ) + + Assert.AreEqual("source", ex.ParamName, "flatList must name the parameter of the default flat list") + + [] + member _.``yield! and for take a flat list of strings that is not default as a sequence of objects`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + + let array = flatList { + yield! widened + + for item in widened do + yield item + } + + CollectionAssert.AreEqual ( + [| "a" :> obj; "b" :> obj; "a" :> obj; "b" :> obj |], + FlatList.toArray array, + "flatList must take the elements of a flat list of strings as objects in yield! and for" + ) + + [] + member _.``while adds the elements while the guard holds`` () = + let mutable count = 0 + + let array = flatList { + while count < 3 do + count <- count + 1 + yield count + } + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + FlatList.toArray array, + "flatList must run the body of while while the guard holds" + ) + + [] + member _.``if without else and following elements combine`` () = + let array = flatList { + if false then + yield 0 + + yield 1 + + if true then + yield 2 + } + + CollectionAssert.AreEqual ([| 1; 2 |], FlatList.toArray array, "flatList must combine conditional and plain elements") + + [] + member _.``try with adds the elements of the handler after an exception`` () = + let array = flatList { + try + yield 1 + failwith "fails" + with _ -> + yield 2 + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + FlatList.toArray array, + "flatList must keep the elements before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let array = flatList { + try + yield 1 + finally + compensated <- true + } + + Assert.IsTrue (compensated, "flatList must run the compensation of try finally") + CollectionAssert.AreEqual ([| 1 |], FlatList.toArray array, "flatList must keep the elements of the body of try finally") + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let array = flatList { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield 1 + } + + Assert.IsTrue (disposed, "flatList must dispose the resource of use") + CollectionAssert.AreEqual ([| 1 |], FlatList.toArray array, "flatList must keep the elements of the body of use") diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListContractTests.fs new file mode 100644 index 00000000..5e013380 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListContractTests.fs @@ -0,0 +1,19 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The sequence-like contract for `FlatList`. +[] +type FlatListSeqLikeTests () = + inherit SeqLikeContract> (FlatListOps.seqLike) + +/// The creation contract for `FlatList`. +[] +type FlatListCreationTests () = + inherit SeqCreationContract> (FlatListOps.creation) + +/// The index-based contract for `FlatList`. +[] +type FlatListIndexedTests () = + inherit IndexedContract> (FlatListOps.indexed) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOps.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOps.fs new file mode 100644 index 00000000..91ec63d4 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOps.fs @@ -0,0 +1,193 @@ +/// The functions of the `FlatList` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.FlatListOps + +open System +open System.Linq +open FSharp.Collections.Immutable + +let private toArrays (arrays : FlatList>) = arrays |> _.Select(FlatList.toArray) |> Seq.toArray + +let private toArrayPair (struct (first : FlatList<'T1>, second : FlatList<'T2>)) = + struct (FlatList.toArray first, FlatList.toArray second) + +/// The functions of the sequence-like facet. `FlatList` names its parameters as the `Array` module does. +let seqLike : SeqOps> = { + Name = nameof FlatList + Mirror = ArrayMirror + ParamName = id + OfSeq = FlatList.ofSeq + ToArray = FlatList.toArray + + IsEmpty = FlatList.isEmpty + Length = FlatList.length + + ExactlyOne = FlatList.exactlyOne + Head = FlatList.head + Last = FlatList.last + Tail = FlatList.tail + TryExactlyOne = FlatList.tryExactlyOne + TryHead = FlatList.tryHead + TryLast = FlatList.tryLast + TryTail = FlatList.tryTail + + Contains = FlatList.contains + Exists = FlatList.exists + Exists2 = FlatList.exists2 + Find = FlatList.find + FindBack = FlatList.findBack + FindIndex = FlatList.findIndex + FindIndexBack = FlatList.findIndexBack + Forall = FlatList.forall + Forall2 = FlatList.forall2 + Pick = FlatList.pick + TryFind = FlatList.tryFind + TryFindBack = FlatList.tryFindBack + TryFindIndex = FlatList.tryFindIndex + TryFindIndexBack = FlatList.tryFindIndexBack + TryPick = FlatList.tryPick + + AllPairs = fun array1 array2 -> FlatList.allPairs array1 array2 |> FlatList.toArray + Append = FlatList.append + Choose = FlatList.choose + ChunkBySize = fun chunkSize array -> FlatList.chunkBySize chunkSize array |> toArrays + Collect = FlatList.collect + Concat = FlatList.concat + Filter = FlatList.filter + Indexed = FlatList.indexed >> FlatList.toArray + Map = FlatList.map + Map2 = FlatList.map2 + Map3 = FlatList.map3 + Mapi = FlatList.mapi + Mapi2 = FlatList.mapi2 + Pairwise = FlatList.pairwise >> FlatList.toArray + Permute = FlatList.permute + Rev = FlatList.rev + Transpose = fun arrays -> FlatList.transpose arrays |> toArrays + Unzip = fun pairs -> FlatList.unzip (FlatList.ofArray pairs) |> toArrayPair + Unzip3 = + fun triples -> + let struct (first, second, third) = FlatList.unzip3 (FlatList.ofArray triples) + struct (FlatList.toArray first, FlatList.toArray second, FlatList.toArray third) + Where = FlatList.where + Windowed = fun windowSize array -> FlatList.windowed windowSize array |> toArrays + Zip = fun array1 array2 -> FlatList.zip array1 array2 |> FlatList.toArray + Zip3 = fun array1 array2 array3 -> FlatList.zip3 array1 array2 array3 |> FlatList.toArray + + Average = fun items -> FlatList.average (FlatList.ofArray items) + AverageBy = FlatList.averageBy + CompareWith = FlatList.compareWith + Fold = FlatList.fold + Fold2 = FlatList.fold2 + FoldBack = FlatList.foldBack + FoldBack2 = FlatList.foldBack2 + Iter = FlatList.iter + Iter2 = FlatList.iter2 + Iteri = FlatList.iteri + Iteri2 = FlatList.iteri2 + MapFold = + fun mapping state array -> + let struct (results, final) = FlatList.mapFold mapping state array + struct (FlatList.toArray results, final) + MapFoldBack = + fun mapping array state -> + let struct (results, final) = FlatList.mapFoldBack mapping array state + struct (FlatList.toArray results, final) + Max = FlatList.max + MaxBy = FlatList.maxBy + Min = FlatList.min + MinBy = FlatList.minBy + Reduce = FlatList.reduce + ReduceBack = FlatList.reduceBack + Scan = fun folder state array -> FlatList.scan folder state array |> FlatList.toArray + ScanBack = fun folder array state -> FlatList.scanBack folder array state |> FlatList.toArray + Sum = FlatList.sum + SumBy = FlatList.sumBy + + CountBy = fun projection array -> FlatList.countBy projection array |> FlatList.toArray + Distinct = FlatList.distinct + DistinctBy = FlatList.distinctBy + Except = FlatList.except + GroupBy = + fun projection array -> + FlatList.groupBy projection array + |> _.Select(fun struct (key, items) -> struct (key, FlatList.toArray items)) + |> Seq.toArray + Partition = FlatList.partition + PartitionWith = fun partitioner array -> FlatList.partitionWith partitioner array |> toArrayPair + Skip = FlatList.skip + SkipLast = FlatList.skipLast + SkipWhile = FlatList.skipWhile + SplitAt = FlatList.splitAt + SplitInto = fun count array -> FlatList.splitInto count array |> toArrays + Take = FlatList.take + TakeLast = FlatList.takeLast + TakeWhile = FlatList.takeWhile + Truncate = FlatList.truncate + + Sort = FlatList.sort + SortBy = FlatList.sortBy + SortByDescending = FlatList.sortByDescending + SortDescending = FlatList.sortDescending + SortWith = FlatList.sortWith + SortWithComparer = FlatList.sortWithComparer + + RandomChoice = FlatList.randomChoice + RandomChoiceBy = FlatList.randomChoiceBy + RandomChoiceWith = FlatList.randomChoiceWith + RandomChoices = FlatList.randomChoices + RandomChoicesBy = FlatList.randomChoicesBy + RandomChoicesWith = FlatList.randomChoicesWith + RandomSample = FlatList.randomSample + RandomSampleBy = FlatList.randomSampleBy + RandomSampleWith = FlatList.randomSampleWith + RandomShuffle = FlatList.randomShuffle + RandomShuffleBy = FlatList.randomShuffleBy + RandomShuffleWith = FlatList.randomShuffleWith + + ToList = FlatList.toList + ToSeq = FlatList.toSeq +} + +/// The functions of the creation facet. +let creation : SeqCreationOps> = { + Name = nameof FlatList + Mirror = ArrayMirror + ParamName = id + ToArray = FlatList.toArray + + Empty = fun () -> FlatList.empty + Init = FlatList.init + OfArray = FlatList.ofArray + OfList = FlatList.ofList + OfReadOnlySpan = fun array -> FlatList.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = FlatList.ofSeq + OfSpan = fun array -> FlatList.ofSpan (Span array) + Replicate = FlatList.replicate + Singleton = FlatList.singleton + Unfold = FlatList.unfold +} + +/// The functions of the index-based facet. +let indexed : IndexedOps> = { + Name = nameof FlatList + Mirror = ArrayMirror + ParamName = id + OfSeq = FlatList.ofSeq + ToArray = FlatList.toArray + + Item = FlatList.item + TryItem = FlatList.tryItem + + IndexOf = FlatList.indexOf + IndexOfWith = FlatList.indexOfWith + LastIndexOf = FlatList.lastIndexOf + LastIndexOfWith = FlatList.lastIndexOfWith + + Add = FlatList.add + InsertAt = FlatList.insertAt + InsertManyAt = FlatList.insertManyAt + RemoveAt = FlatList.removeAt + RemoveManyAt = FlatList.removeManyAt + UpdateAt = FlatList.updateAt +} diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOptionTests.fs new file mode 100644 index 00000000..17554b8b --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOptionTests.fs @@ -0,0 +1,165 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open FSharp.Collections.Immutable.Option +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The namespaces are opened in the order that gives the shapes of FSharp.Core: Tuple first, Option last. +// The type annotations of the results are the proof that the functions of the Option namespace shadow the default ones. + +/// The `FlatList` module of the `Option` namespace. +[] +type FlatListOptionTests () = + + static let array = FlatList.ofArray Inputs.Several + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + [] + member _.``unfold takes a generator of options of reference tuples`` () = + let generator (state : int) = if state > 0 then Some (state, state - 1) else None + let generated : FlatList = FlatList.unfold generator 3 + + CollectionAssert.AreEqual ( + Array.unfold generator 3, + FlatList.toArray generated, + "Option.FlatList.unfold must generate as Array.unfold does" + ) + + [] + member _.``tryExactlyOne returns an option`` () = + let found : int option = FlatList.tryExactlyOne (FlatList.singleton 5) + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryExactlyOne (FlatList.singleton 5), + found, + "Option.FlatList.tryExactlyOne must match the default after conversion" + ) + + [] + member _.``tryHead returns an option`` () = + let found : int option = FlatList.tryHead array + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryHead array, + found, + "Option.FlatList.tryHead must match the default" + ) + + [] + member _.``tryItem returns an option`` (index : int) = + let found : int option = FlatList.tryItem index array + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryItem index array, + found, + "Option.FlatList.tryItem must match the default" + ) + + [] + member _.``tryLast returns an option`` () = + let found : int option = FlatList.tryLast array + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryLast array, + found, + "Option.FlatList.tryLast must match the default" + ) + + [] + member _.``tryTail returns an option`` () = + let expected = FSharp.Collections.Immutable.FlatList.tryTail array + let found : FlatList option = FlatList.tryTail array + Assert.AreEqual(expected.IsSome, found.IsSome, "Option.FlatList.tryTail must return a tail when the default does") + + // The tails are compared element by element + match expected, found with + | ValueSome expected, Some found -> + CollectionAssert.AreEqual ( + FlatList.toArray expected, + FlatList.toArray found, + "Option.FlatList.tryTail must return the tail of the default" + ) + | _ -> () + + [] + member _.``pick takes a chooser of options`` () = + let picked : int = FlatList.pick negatedIfNegative array + Assert.AreEqual( + Array.pick negatedIfNegative Inputs.Several, + picked, + "Option.FlatList.pick must pick as Array.pick does" + ) + + [] + member _.``tryFind returns an option`` () = + let found : int option = FlatList.tryFind (fun item -> item > 6) array + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryFind (fun item -> item > 6) array, + found, + "Option.FlatList.tryFind must match the default" + ) + + [] + member _.``tryFindBack returns an option`` () = + let found : int option = FlatList.tryFindBack (fun item -> item > 6) array + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryFindBack (fun item -> item > 6) array, + found, + "Option.FlatList.tryFindBack must match the default" + ) + + [] + member _.``tryFindIndex returns an option`` () = + let found : int option = FlatList.tryFindIndex (fun item -> item > 6) array + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryFindIndex (fun item -> item > 6) array, + found, + "Option.FlatList.tryFindIndex must match the default" + ) + + [] + member _.``tryFindIndexBack returns an option`` () = + let found : int option = FlatList.tryFindIndexBack (fun item -> item > 6) array + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.FlatList.tryFindIndexBack (fun item -> item > 6) array, + found, + "Option.FlatList.tryFindIndexBack must match the default" + ) + + [] + member _.``tryPick takes a chooser of options and returns an option`` () = + let found : int option = FlatList.tryPick negatedIfNegative array + Assert.AreEqual( + Array.tryPick negatedIfNegative Inputs.Several, + found, + "Option.FlatList.tryPick must pick as Array.tryPick does" + ) + + [] + member _.``choose takes a chooser of options`` () = + let chosen : FlatList = FlatList.choose negatedIfNegative array + + CollectionAssert.AreEqual ( + Array.choose negatedIfNegative Inputs.Several, + FlatList.toArray chosen, + "Option.FlatList.choose must choose as Array.choose does" + ) + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let mapped : FlatList = FlatList.map (fun item -> item * 2) array + let zipped : FlatList = FlatList.zip array array + CollectionAssert.AreEqual ( + Array.map (fun item -> item * 2) Inputs.Several, + FlatList.toArray mapped, + "FlatList.map must resolve to the default module" + ) + CollectionAssert.AreEqual ( + Array.zip Inputs.Several Inputs.Several, + FlatList.toArray zipped, + "FlatList.zip must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOracles.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOracles.fs new file mode 100644 index 00000000..e55f5207 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListOracles.fs @@ -0,0 +1,99 @@ +/// The expectations that the tests of the flat list and of its builder compute in plain F#, over arrays. +[] +module FSharp.Collections.Immutable.Tests.FlatListOracles + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable + +/// A flat list of strings as a sequence of objects. A flat list of strings is also a sequence of objects, by the +/// covariance of seq: the element type of the sequence is not the one of the flat list, and a type test for the flat +/// list of objects does not see it. +let stringsAsObjects (strings : FlatList) : seq = + match box strings with + | :? seq as widened -> widened + | _ -> invalidOp "A flat list of strings must be a sequence of objects" + +/// A default flat list of strings as a sequence of objects. +let defaultStringsAsObjects : seq = stringsAsObjects Unchecked.defaultof> +/// A builder that holds a copy of the elements. +let builderOf (elements : 'T array) = FlatList.ofArray elements |> FlatList.toBuilder + +/// The elements without the first one that the equality finds equal to the item: the oracle of the removal by value. +let removeFirst (equal : 'T -> 'T -> bool) (item : 'T) (elements : 'T array) = + match Array.tryFindIndex (equal item) elements with + | Some index -> Array.removeAt index elements + | None -> elements + +/// The elements with the first one that the equality finds equal to the old value replaced by the new value, or +/// ValueNone when no element is equal: the oracle of the replacement. +let replaceFirst (equal : 'T -> 'T -> bool) (oldValue : 'T) (newValue : 'T) (elements : 'T array) = + match Array.tryFindIndex (equal oldValue) elements with + | Some index -> ValueSome (Array.updateAt index newValue elements) + | None -> ValueNone + +/// Tells whether two strings are equal when the case is ignored. +let equalIgnoringCase (left : string) (right : string) = String.Equals (left, right, StringComparison.OrdinalIgnoreCase) + +/// The strings of the removal by value; "b" and "B" are equal when the case is ignored. +let names () = [| "a"; "b"; "B"; "c" |] + +/// The strings of the searches of a range: "b" is there twice, "B" once, and the three are equal when the case is +/// ignored. +let rangeNames () = [| "a"; "b"; "B"; "c"; "b" |] + +/// The comparison of the descending order. +let descending (left : int) (right : int) = compare right left + +/// The comparison of the order of the absolute values. +let byAbsolute (left : int) (right : int) = compare (abs left) (abs right) + +/// The lines of the message of the exception that an action throws after the first line, which is empty when the +/// message has one line. FSharp.Core writes the names and the values of the arguments there, and it does not localize +/// them. The action must throw. +let messageDetail (action : unit -> unit) = + let thrown = + try + action () + ValueNone + with ex -> + ValueSome ex + + match thrown with + | ValueSome ex -> + match ex.Message.IndexOf '\n' with + | -1 -> "" + | newline -> ex.Message.Substring (newline + 1) + | ValueNone -> invalidOp "The action must throw" + +/// The index of the first element of a range that the equality finds equal to the value, or -1: the oracle of the +/// searches from the front. +let firstInRange (equal : 'T -> 'T -> bool) startIndex count (value : 'T) (elements : 'T array) = + seq { startIndex .. startIndex + count - 1 } + |> Seq.tryFind (fun index -> equal elements[index] value) + |> Option.defaultValue -1 + +/// The index of the first element that the equality finds equal to the value, searching backward from the start index +/// over the count of elements, or -1: the oracle of the searches from the back. +let lastInRange (equal : 'T -> 'T -> bool) startIndex count (value : 'T) (elements : 'T array) = + seq { startIndex .. -1 .. startIndex - count + 1 } + |> Seq.tryFind (fun index -> equal elements[index] value) + |> Option.defaultValue -1 + +/// The result of a binary search of a range of elements that a comparer sorts and that are all different: the index of +/// the value, or the complement of the index of the first larger element of the range. +let searchRange (comparer : IComparer<'T>) index count (value : 'T) (sorted : 'T array) = + let range = seq { index .. index + count - 1 } + + match + range + |> Seq.tryFind (fun at -> comparer.Compare (sorted[at], value) = 0) + with + | Some found -> found + | None -> + let smaller = + range + |> Seq.filter (fun at -> comparer.Compare (sorted[at], value) < 0) + |> Seq.length + + ~~~(index + smaller) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListSearchTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListSearchTests.fs new file mode 100644 index 00000000..2fd5f812 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListSearchTests.fs @@ -0,0 +1,460 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Collections.Immutable +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The searches of a range and the binary searches of `FlatList`, and the searches of a range of its builder. +[] +type FlatListSearchTests () = + + /// The inputs of the searches of a range: none, one element and several, with a value twice and in two cases. + static let searchInputs () = [| [||]; [| "b" |]; FlatListOracles.rangeNames () |] + + /// The values that the searches of a range look for: in the case of the elements, in another case, and a value that + /// no input has. + static let valuesToFind = [| "B"; "b"; "Z" |] + + /// Every valid range for the searches from the front. The start index is less than the length, as the BCL of .NET 8 + /// and .NET 10 requires, except for the empty range at the start index 0. + static let frontRanges (length : int) = seq { + for startIndex in 0 .. max 0 (length - 1) do + for count in 0 .. length - startIndex do + struct (startIndex, count) + } + + /// Every valid range for the searches from the back: from the start index down over the count of elements. + static let backRanges (length : int) = seq { + for startIndex in 0 .. max 0 (length - 1) do + for count in 0 .. min (startIndex + 1) length do + struct (startIndex, count) + } + + /// The searches of a range from the front, by name, with the function, the BCL member that it wraps and the equality + /// that it searches in. The functions with a comparer get the comparer that ignores the case. + static let frontSearches + : Map< + string, + struct ((string array -> int -> int -> string -> int) * + (string array -> int -> int -> string -> int) * + (string -> string -> bool)) + > = + let comparer = StringComparer.OrdinalIgnoreCase + + Map.ofArray [| + "FlatList.indexOfRange", + struct ((fun input startIndex count value -> FlatList.indexOfRange startIndex count value (FlatList.ofArray input)), + (fun input startIndex count value -> (FlatList.ofArray input).IndexOf(value, startIndex, count)), + (=)) + "FlatList.indexOfRangeWith", + struct ((fun input startIndex count value -> + FlatList.indexOfRangeWith comparer startIndex count value (FlatList.ofArray input) + ), + (fun input startIndex count value -> (FlatList.ofArray input).IndexOf(value, startIndex, count, comparer)), + FlatListOracles.equalIgnoringCase) + "FlatListBuilder.indexOfRange", + struct ((fun input startIndex count value -> + FlatListBuilder.indexOfRange startIndex count value (FlatListOracles.builderOf input) + ), + (fun input startIndex count value -> (FlatListOracles.builderOf input).IndexOf(value, startIndex, count)), + (=)) + "FlatListBuilder.indexOfRangeWith", + struct ((fun input startIndex count value -> + FlatListBuilder.indexOfRangeWith comparer startIndex count value (FlatListOracles.builderOf input) + ), + (fun input startIndex count value -> + (FlatListOracles.builderOf input).IndexOf(value, startIndex, count, comparer) + ), + FlatListOracles.equalIgnoringCase) + |] + + /// The searches of a range from the back, as the table of the searches from the front. + static let backSearches + : Map< + string, + struct ((string array -> int -> int -> string -> int) * + (string array -> int -> int -> string -> int) * + (string -> string -> bool)) + > = + let comparer = StringComparer.OrdinalIgnoreCase + + Map.ofArray [| + "FlatList.lastIndexOfRange", + struct ((fun input startIndex count value -> + FlatList.lastIndexOfRange startIndex count value (FlatList.ofArray input) + ), + (fun input startIndex count value -> (FlatList.ofArray input).LastIndexOf(value, startIndex, count)), + (=)) + "FlatList.lastIndexOfRangeWith", + struct ((fun input startIndex count value -> + FlatList.lastIndexOfRangeWith comparer startIndex count value (FlatList.ofArray input) + ), + (fun input startIndex count value -> (FlatList.ofArray input).LastIndexOf(value, startIndex, count, comparer)), + FlatListOracles.equalIgnoringCase) + "FlatListBuilder.lastIndexOfRange", + struct ((fun input startIndex count value -> + FlatListBuilder.lastIndexOfRange startIndex count value (FlatListOracles.builderOf input) + ), + (fun input startIndex count value -> (FlatListOracles.builderOf input).LastIndexOf(value, startIndex, count)), + (=)) + "FlatListBuilder.lastIndexOfRangeWith", + struct ((fun input startIndex count value -> + FlatListBuilder.lastIndexOfRangeWith comparer startIndex count value (FlatListOracles.builderOf input) + ), + (fun input startIndex count value -> + (FlatListOracles.builderOf input).LastIndexOf(value, startIndex, count, comparer) + ), + FlatListOracles.equalIgnoringCase) + |] + + /// The elements of the binary searches: sorted, ascending, and all different. + static let sortedValues () = [| 1; 3; 5; 7; 9; 11; 13; 15 |] + + /// The values that the binary searches look for: below every element, in the elements when the range has them, between + /// two elements, and above every element. + static let searchValues = [| 0; 7; 8; 16 |] + + /// Strings that are sorted and different when the case is ignored. The ordinal order puts "B" and "D" before "a". + static let sortedNames () = [| "a"; "B"; "c"; "D"; "e" |] + + /// The binary searches of a range, by name, with an input that the comparer of the search sorts, the comparer, and the + /// function on the input. + static let binarySearchRanges : Map * (int array -> int -> int -> int -> int))> = + let descending = Comparer.Create(Comparison FlatListOracles.descending) + + Map.ofArray [| + nameof FlatList.binarySearchRange, + struct (sortedValues (), + Comparer.Default :> IComparer, + (fun sorted index count value -> FlatList.binarySearchRange index count value (FlatList.ofArray sorted))) + nameof FlatList.binarySearchRangeWithComparer, + struct (Array.rev (sortedValues ()), + descending :> IComparer, + (fun sorted index count value -> + FlatList.binarySearchRangeWithComparer descending index count value (FlatList.ofArray sorted) + )) + |] + + /// Every valid range of every search from the front on every input, with each value to find. + static member FrontRanges = seq { + for name in frontSearches.Keys do + for input in searchInputs () do + for struct (startIndex, count) in frontRanges input.Length do + for value in valuesToFind do + struct (name, input, startIndex, count, value) + } + + /// Every valid range of every search from the back on every input, with each value to find. + static member BackRanges = seq { + for name in backSearches.Keys do + for input in searchInputs () do + for struct (startIndex, count) in backRanges input.Length do + for value in valuesToFind do + struct (name, input, startIndex, count, value) + } + + /// The ranges that the searches from the front reject: a start index that is negative or not less than the length, + /// except for the range 0, 0, and a count that is negative or beyond the end. The BCL of .NET 11 accepts an empty + /// range at a start index that equals the length, so the rows with such a range do not throw there. The project + /// targets .NET 8 and .NET 10. + static member FrontRangesOutOfRange = seq { + let ranges = [| + struct ([||], 0, 1) + struct ([||], 1, 0) + struct ([||], -1, 0) + struct ([| "b" |], 1, 0) + struct ([| "b" |], 0, 2) + struct (FlatListOracles.rangeNames (), 5, 0) + struct (FlatListOracles.rangeNames (), 6, 0) + struct (FlatListOracles.rangeNames (), -1, 0) + struct (FlatListOracles.rangeNames (), -1, 2) + struct (FlatListOracles.rangeNames (), 0, -1) + struct (FlatListOracles.rangeNames (), 0, 6) + struct (FlatListOracles.rangeNames (), 2, 4) + struct (FlatListOracles.rangeNames (), 5, 1) + struct (FlatListOracles.rangeNames (), 3, 3) + |] + + for name in frontSearches.Keys do + for struct (input, startIndex, count) in ranges do + struct (name, input, startIndex, count) + } + + /// The ranges that the searches from the back reject: a start index that is negative or not less than the length, + /// except for the range 0, 0, and a count that is negative or beyond the start index plus one. + static member BackRangesOutOfRange = seq { + let ranges = [| + struct ([||], 0, 1) + struct ([||], 1, 0) + struct ([||], -1, 0) + struct ([| "b" |], 1, 0) + struct ([| "b" |], 0, 2) + struct (FlatListOracles.rangeNames (), 5, 1) + struct (FlatListOracles.rangeNames (), 5, 0) + struct (FlatListOracles.rangeNames (), -1, 0) + struct (FlatListOracles.rangeNames (), 3, 5) + struct (FlatListOracles.rangeNames (), 3, -1) + struct (FlatListOracles.rangeNames (), 0, 2) + struct (FlatListOracles.rangeNames (), 4, 6) + struct (FlatListOracles.rangeNames (), 2, 4) + |] + + for name in backSearches.Keys do + for struct (input, startIndex, count) in ranges do + struct (name, input, startIndex, count) + } + + /// Every valid range of the sorted integers, with each value to search for, for each binary search of a range. + static member BinarySearchRanges = seq { + let length = (sortedValues ()).Length + + for name in binarySearchRanges.Keys do + for index in 0..length do + for count in 0 .. length - index do + for value in searchValues do + struct (name, index, count, value) + } + + /// Every range that the binary searches of a range reject: a negative index or count, and a range beyond the end. + static member BinarySearchRangesOutOfRange = seq { + let ranges = [| + struct (-1, 2) + struct (0, -1) + struct (-1, -1) + struct (9, 0) + struct (8, 1) + struct (0, 9) + struct (4, 5) + struct (7, 2) + struct (3, 6) + |] + + for name in binarySearchRanges.Keys do + for struct (index, count) in ranges do + struct (name, index, count) + } + + /// Every valid range of the sorted strings, with each value to search for. + static member BinarySearchRangesOfNames = seq { + let length = (sortedNames ()).Length + + for index in 0..length do + for count in 0 .. length - index do + for value in [| "b"; "D"; "z" |] do + struct (index, count, value) + } + + /// The values to search for in the sorted integers. + static member SearchValues = searchValues + + /// The values to search for in the sorted strings: in the other case, in the same case, below, and above. + static member SearchNames = [| "A"; "b"; "C"; "d"; "E"; "f"; "0" |] + + [] + member _.``range searches from the front return the first equal element of the range`` + (name : string, input : string array, startIndex : int, count : int, value : string) + = + let struct (search, _, equal) = frontSearches[name] + + Assert.AreEqual( + FlatListOracles.firstInRange equal startIndex count value input, + search input startIndex count value, + $"%s{name} must return the index of the first equal element of the range" + ) + + [] + member _.``range searches from the back return the last equal element of the range`` + (name : string, input : string array, startIndex : int, count : int, value : string) + = + let struct (search, _, equal) = backSearches[name] + + Assert.AreEqual( + FlatListOracles.lastInRange equal startIndex count value input, + search input startIndex count value, + $"%s{name} must return the index of the last equal element of the range" + ) + + [] + member _.``range searches from the front throw for a range out of range as the BCL does`` + (name : string, input : string array, startIndex : int, count : int) + = + let struct (search, bcl, _) = frontSearches[name] + + ExceptionAssert.Same ( + id, + (fun () -> bcl input startIndex count "b" |> ignore), + (fun () -> search input startIndex count "b" |> ignore), + $"%s{name} must throw as the BCL member does for a range out of range" + ) + + [] + member _.``range searches from the back throw for a range out of range as the BCL does`` + (name : string, input : string array, startIndex : int, count : int) + = + let struct (search, bcl, _) = backSearches[name] + + ExceptionAssert.Same ( + id, + (fun () -> bcl input startIndex count "b" |> ignore), + (fun () -> search input startIndex count "b" |> ignore), + $"%s{name} must throw as the BCL member does for a range out of range" + ) + + [] + member _.``range searches with a comparer take null for the default comparer`` (name : string) = + let names = FlatListOracles.rangeNames () + let array = FlatList.ofArray names + let comparer = Unchecked.defaultof> + + // "B" is there once, at the index 2, where the ignoring comparer would find "b" at the index 1 or 4 + let found, expected = + match name with + | "indexOfRangeWith" -> FlatList.indexOfRangeWith comparer 0 5 "B" array, 2 + | _ -> FlatList.lastIndexOfRangeWith comparer 4 5 "B" array, 2 + + Assert.AreEqual(expected, found, $"FlatList.%s{name} must compare in the default equality for a null comparer") + + let builder = FlatListOracles.builderOf names + + let foundInBuilder = + match name with + | "indexOfRangeWith" -> FlatListBuilder.indexOfRangeWith comparer 0 5 "B" builder + | _ -> FlatListBuilder.lastIndexOfRangeWith comparer 4 5 "B" builder + + Assert.AreEqual( + expected, + foundInBuilder, + $"FlatListBuilder.%s{name} must compare in the default equality for a null comparer" + ) + + [] + member _.``binary searches of a range find the value or the complement of its insertion point in the range`` + (name : string, index : int, count : int, value : int) + = + let struct (sorted, comparer, search) = binarySearchRanges[name] + + Assert.AreEqual( + FlatListOracles.searchRange comparer index count value sorted, + search sorted index count value, + $"FlatList.%s{name} must search the range in the order of its comparison" + ) + + [] + member _.``binary searches of a range throw for a range out of range as the BCL does`` + (name : string, index : int, count : int) + = + let struct (sorted, comparer, search) = binarySearchRanges[name] + + // Array.BinarySearch calls the count of the range length, and the functions call it count + let countOfLength (parameter : string) = + match parameter with + | "length" -> "count" + | other -> other + + ExceptionAssert.Same ( + countOfLength, + (fun () -> + ImmutableArray.BinarySearch (FlatList.ofArray sorted, index, count, 7, comparer) + |> ignore + ), + (fun () -> search sorted index count 7 |> ignore), + $"FlatList.%s{name} must throw as the BCL does, with the parameter name count" + ) + + [] + member _.``binarySearchRange compares strings as F# generic comparison does, which is ordinal`` + (index : int, count : int, value : string, expected : int) + = + // The ordinal order of these strings is not the order of the current culture, which Comparer.Default would use + let sorted = FlatList.ofArray [| "A"; "B"; "a"; "b" |] + + Assert.AreEqual( + expected, + FlatList.binarySearchRange index count value sorted, + "FlatList.binarySearchRange must search in the ordinal order of the strings" + ) + + [] + member _.``binarySearchRangeWithComparer searches the range in the order of the comparer`` + (index : int, count : int, value : string) + = + let sorted = sortedNames () + + Assert.AreEqual( + FlatListOracles.searchRange StringComparer.OrdinalIgnoreCase index count value sorted, + FlatList.binarySearchRangeWithComparer StringComparer.OrdinalIgnoreCase index count value (FlatList.ofArray sorted), + "FlatList.binarySearchRangeWithComparer must search in the order of the comparer" + ) + + [] + member _.``binarySearchWithComparer searches the flat list in the order of the comparer`` (value : int) = + let descending = Comparer.Create(Comparison FlatListOracles.descending) + let sorted = Array.rev (sortedValues ()) + + Assert.AreEqual( + FlatListOracles.searchRange descending 0 sorted.Length value sorted, + FlatList.binarySearchWithComparer descending value (FlatList.ofArray sorted), + "FlatList.binarySearchWithComparer must search in the order of the comparer" + ) + + [] + member _.``binarySearchWithComparer finds a string that differs in case when the comparer ignores the case`` + (value : string) + = + let sorted = sortedNames () + + Assert.AreEqual( + FlatListOracles.searchRange StringComparer.OrdinalIgnoreCase 0 sorted.Length value sorted, + FlatList.binarySearchWithComparer StringComparer.OrdinalIgnoreCase value (FlatList.ofArray sorted), + "FlatList.binarySearchWithComparer must compare as the comparer does" + ) + + [] + member _.``binary searches with a comparer take null for the default comparer`` (value : int) = + let sorted = FlatList.ofArray (sortedValues ()) + let comparer = Unchecked.defaultof> + let expected = FlatListOracles.searchRange Comparer.Default 0 sorted.Length value (sortedValues ()) + + Assert.AreEqual( + expected, + FlatList.binarySearchWithComparer comparer value sorted, + "FlatList.binarySearchWithComparer must compare in the default order for a null comparer" + ) + + Assert.AreEqual( + expected, + FlatList.binarySearchRangeWithComparer comparer 0 sorted.Length value sorted, + "FlatList.binarySearchRangeWithComparer must compare in the default order for a null comparer" + ) + + [] + member _.``binary searches with a comparer that throws throw InvalidOperationException with the exception inside`` + (name : string) + = + let failure = InvalidOperationException "the comparer fails" + + let comparer = { + new IComparer with + member _.Compare (_, _) = raise failure + } + + let sorted = FlatList.ofArray (sortedValues ()) + + let search () = + match name with + | "binarySearchWithComparer" -> FlatList.binarySearchWithComparer comparer 7 sorted + | _ -> FlatList.binarySearchRangeWithComparer comparer 0 8 7 sorted + + let ex = + Assert.ThrowsExactly( + Action (search >> ignore), + $"FlatList.%s{name} must throw InvalidOperationException for a comparer that throws" + ) + + Assert.AreSame( + failure, + ex.InnerException, + $"FlatList.%s{name} must keep the exception of the comparer inside" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTests.fs new file mode 100644 index 00000000..560bd80e --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTests.fs @@ -0,0 +1,1164 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Runtime.InteropServices +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The behaviour of `FlatList` that the shared contracts do not cover: default values, shared storage and the extras. +[] +type FlatListTests () = + + static let defaultArray = Unchecked.defaultof> + + static let nullArray = Unchecked.defaultof + + /// The array under a flat list, to tell whether two flat lists share their storage. + static let underlying (array : FlatList<'T>) = ImmutableCollectionsMarshal.AsArray array + + + /// Maps the parameter names of the Array oracles to the ones of the flat list functions that name a sequence of + /// values: Array.insertManyAt names its values source, as Seq.toArray does, where the flat list function names them values. + static let valuesParamName (name : string) = + match name with + | "source" -> "values" + | other -> other + + + /// The functions that mirror an Array function, by name, with that function on a null array and the function on a + /// default flat list. The rows of the test are the names: a function has no readable name in a row. + static let arrayRejections = + [| + struct (nameof FlatList.allPairs, + (fun () -> Array.allPairs nullArray [| 1 |] |> ignore), + (fun () -> + FlatList.allPairs defaultArray (FlatList.singleton 1) + |> ignore + )) + struct (nameof FlatList.append, + (fun () -> Array.append [| 1 |] nullArray |> ignore), + (fun () -> + FlatList.append (FlatList.singleton 1) defaultArray + |> ignore + )) + struct (nameof FlatList.average, + (fun () -> Array.average (Unchecked.defaultof) |> ignore), + (fun () -> + FlatList.average (Unchecked.defaultof>) + |> ignore + )) + struct (nameof FlatList.choose, + (fun () -> Array.choose Some nullArray |> ignore), + (fun () -> FlatList.choose ValueSome defaultArray |> ignore)) + struct (nameof FlatList.chunkBySize, + (fun () -> Array.chunkBySize 2 nullArray |> ignore), + (fun () -> FlatList.chunkBySize 2 defaultArray |> ignore)) + struct (nameof FlatList.contains, + (fun () -> Array.contains 1 nullArray |> ignore), + (fun () -> FlatList.contains 1 defaultArray |> ignore)) + struct (nameof FlatList.countBy, + (fun () -> Array.countBy id nullArray |> ignore), + (fun () -> FlatList.countBy id defaultArray |> ignore)) + struct (nameof FlatList.distinct, + (fun () -> Array.distinct nullArray |> ignore), + (fun () -> FlatList.distinct defaultArray |> ignore)) + struct (nameof FlatList.exactlyOne, + (fun () -> Array.exactlyOne nullArray |> ignore), + (fun () -> FlatList.exactlyOne defaultArray |> ignore)) + struct (nameof FlatList.except, + (fun () -> Array.except [ 1 ] nullArray |> ignore), + (fun () -> FlatList.except [ 1 ] defaultArray |> ignore)) + struct (nameof FlatList.exists, + (fun () -> Array.exists ((=) 1) nullArray |> ignore), + (fun () -> FlatList.exists ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.filter, + (fun () -> Array.filter ((=) 1) nullArray |> ignore), + (fun () -> FlatList.filter ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.fold, + (fun () -> Array.fold (+) 0 nullArray |> ignore), + (fun () -> FlatList.fold (+) 0 defaultArray |> ignore)) + struct (nameof FlatList.groupBy, + (fun () -> Array.groupBy id nullArray |> ignore), + (fun () -> FlatList.groupBy id defaultArray |> ignore)) + struct (nameof FlatList.head, + (fun () -> Array.head nullArray |> ignore), + (fun () -> FlatList.head defaultArray |> ignore)) + struct (nameof FlatList.indexed, + (fun () -> Array.indexed nullArray |> ignore), + (fun () -> FlatList.indexed defaultArray |> ignore)) + struct (nameof FlatList.insertAt, + (fun () -> Array.insertAt 0 1 nullArray |> ignore), + (fun () -> FlatList.insertAt 0 1 defaultArray |> ignore)) + struct (nameof FlatList.isEmpty, + (fun () -> Array.isEmpty nullArray |> ignore), + (fun () -> FlatList.isEmpty defaultArray |> ignore)) + struct (nameof FlatList.iter, (fun () -> Array.iter ignore nullArray), (fun () -> FlatList.iter ignore defaultArray)) + struct (nameof FlatList.map, + (fun () -> Array.map id nullArray |> ignore), + (fun () -> FlatList.map id defaultArray |> ignore)) + struct (nameof FlatList.mapFold, + (fun () -> Array.mapFold (fun s x -> x, s) 0 nullArray |> ignore), + (fun () -> + FlatList.mapFold (fun s x -> struct (x, s)) 0 defaultArray + |> ignore + )) + struct (nameof FlatList.max, + (fun () -> Array.max nullArray |> ignore), + (fun () -> FlatList.max defaultArray |> ignore)) + struct (nameof FlatList.pairwise, + (fun () -> Array.pairwise nullArray |> ignore), + (fun () -> FlatList.pairwise defaultArray |> ignore)) + struct (nameof FlatList.partition, + (fun () -> Array.partition ((=) 1) nullArray |> ignore), + (fun () -> FlatList.partition ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.pick, + (fun () -> Array.pick Some nullArray |> ignore), + (fun () -> FlatList.pick ValueSome defaultArray |> ignore)) + struct (nameof FlatList.randomChoice, + (fun () -> Array.randomChoice nullArray |> ignore), + (fun () -> FlatList.randomChoice defaultArray |> ignore)) + struct (nameof FlatList.rev, + (fun () -> Array.rev nullArray |> ignore), + (fun () -> FlatList.rev defaultArray |> ignore)) + struct (nameof FlatList.skip, + (fun () -> Array.skip 0 nullArray |> ignore), + (fun () -> FlatList.skip 0 defaultArray |> ignore)) + struct (nameof FlatList.sort, + (fun () -> Array.sort nullArray |> ignore), + (fun () -> FlatList.sort defaultArray |> ignore)) + struct (nameof FlatList.splitAt, + (fun () -> Array.splitAt 0 nullArray |> ignore), + (fun () -> FlatList.splitAt 0 defaultArray |> ignore)) + struct (nameof FlatList.sum, + (fun () -> Array.sum nullArray |> ignore), + (fun () -> FlatList.sum defaultArray |> ignore)) + struct (nameof FlatList.tail, + (fun () -> Array.tail nullArray |> ignore), + (fun () -> FlatList.tail defaultArray |> ignore)) + struct (nameof FlatList.take, + (fun () -> Array.take 0 nullArray |> ignore), + (fun () -> FlatList.take 0 defaultArray |> ignore)) + struct (nameof FlatList.toList, + (fun () -> Array.toList nullArray |> ignore), + (fun () -> FlatList.toList defaultArray |> ignore)) + struct (nameof FlatList.tryFind, + (fun () -> Array.tryFind ((=) 1) nullArray |> ignore), + (fun () -> FlatList.tryFind ((=) 1) defaultArray |> ignore)) + struct (nameof FlatList.tryHead, + (fun () -> Array.tryHead nullArray |> ignore), + (fun () -> FlatList.tryHead defaultArray |> ignore)) + struct (nameof FlatList.unzip, + (fun () -> + Array.unzip (Unchecked.defaultof<(int * int) array>) + |> ignore + ), + (fun () -> + FlatList.unzip (Unchecked.defaultof>) + |> ignore + )) + struct (nameof FlatList.windowed, + (fun () -> Array.windowed 2 nullArray |> ignore), + (fun () -> FlatList.windowed 2 defaultArray |> ignore)) + struct (nameof FlatList.zip, + (fun () -> Array.zip [| 1 |] nullArray |> ignore), + (fun () -> FlatList.zip (FlatList.singleton 1) defaultArray |> ignore)) + |] + |> Array.map (fun struct (name, oracle, actual) -> name, struct (oracle, actual)) + |> Map.ofArray + + /// The functions without an Array counterpart, by name, with the parameter that names the default flat list that they + /// reject and the function on a default flat list. + static let otherRejections = + [| + struct (nameof FlatList.add, "array", (fun () -> FlatList.add 1 defaultArray |> ignore)) + struct (nameof FlatList.asMemory, "array", (fun () -> FlatList.asMemory defaultArray |> ignore)) + struct (nameof FlatList.binarySearch, "array", (fun () -> FlatList.binarySearch 1 defaultArray |> ignore)) + // The default flat list is checked before the range, which would be rejected as well + struct (nameof FlatList.binarySearchRange, + "array", + (fun () -> FlatList.binarySearchRange -1 -1 1 defaultArray |> ignore)) + struct (nameof FlatList.binarySearchRangeWithComparer, + "array", + (fun () -> + FlatList.binarySearchRangeWithComparer Comparer.Default -1 -1 1 defaultArray + |> ignore + )) + struct (nameof FlatList.binarySearchWithComparer, + "array", + (fun () -> + FlatList.binarySearchWithComparer Comparer.Default 1 defaultArray + |> ignore + )) + struct (nameof FlatList.blit, "source", (fun () -> FlatList.blit defaultArray 0 [| 0 |] 0 0)) + // Array.concat dereferences a null element array without a check; a default element is a default flat list (D6) + struct (nameof FlatList.concat, "arrays", (fun () -> FlatList.concat [ defaultArray ] |> ignore)) + struct (nameof FlatList.transpose, "arrays", (fun () -> FlatList.transpose [ defaultArray ] |> ignore)) + // Array.collect dereferences a null result of the mapping; a default result is rejected as in concat + struct (nameof FlatList.collect, + "mapping", + (fun () -> + FlatList.collect (fun _ -> defaultArray) (FlatList.singleton 1) + |> ignore + )) + // A default flat list passed as a sequence is still a default flat list + struct (nameof FlatList.ofSeq, "source", (fun () -> FlatList.ofSeq (defaultArray :> seq) |> ignore)) + struct (nameof FlatList.indexOf, "array", (fun () -> FlatList.indexOf 1 defaultArray |> ignore)) + struct (nameof FlatList.indexOfRange, "array", (fun () -> FlatList.indexOfRange -1 -1 1 defaultArray |> ignore)) + struct (nameof FlatList.indexOfRangeWith, + "array", + (fun () -> + FlatList.indexOfRangeWith EqualityComparer.Default -1 -1 1 defaultArray + |> ignore + )) + struct (nameof FlatList.indexOfWith, + "array", + (fun () -> + FlatList.indexOfWith EqualityComparer.Default 1 defaultArray + |> ignore + )) + struct (nameof FlatList.lastIndexOf, "array", (fun () -> FlatList.lastIndexOf 1 defaultArray |> ignore)) + struct (nameof FlatList.lastIndexOfRange, + "array", + (fun () -> FlatList.lastIndexOfRange -1 -1 1 defaultArray |> ignore)) + struct (nameof FlatList.lastIndexOfRangeWith, + "array", + (fun () -> + FlatList.lastIndexOfRangeWith EqualityComparer.Default -1 -1 1 defaultArray + |> ignore + )) + struct (nameof FlatList.lastIndexOfWith, + "array", + (fun () -> + FlatList.lastIndexOfWith EqualityComparer.Default 1 defaultArray + |> ignore + )) + struct (nameof FlatList.length, "array", (fun () -> FlatList.length defaultArray |> ignore)) + struct (nameof FlatList.remove, "source", (fun () -> FlatList.remove 1 defaultArray |> ignore)) + struct (nameof FlatList.removeMany, "source", (fun () -> FlatList.removeMany [| 1 |] defaultArray |> ignore)) + struct (nameof FlatList.removeManyWith, + "source", + (fun () -> + FlatList.removeManyWith EqualityComparer.Default [| 1 |] defaultArray + |> ignore + )) + struct (nameof FlatList.removeWith, + "source", + (fun () -> + FlatList.removeWith EqualityComparer.Default 1 defaultArray + |> ignore + )) + struct (nameof FlatList.replace, "array", (fun () -> FlatList.replace 1 2 defaultArray |> ignore)) + struct (nameof FlatList.replaceWith, + "array", + (fun () -> + FlatList.replaceWith EqualityComparer.Default 1 2 defaultArray + |> ignore + )) + struct (nameof FlatList.skipLast, "array", (fun () -> FlatList.skipLast 0 defaultArray |> ignore)) + struct (nameof FlatList.sortRange, "array", (fun () -> FlatList.sortRange 0 0 defaultArray |> ignore)) + struct (nameof FlatList.sortRangeWith, "array", (fun () -> FlatList.sortRangeWith compare 0 0 defaultArray |> ignore)) + struct (nameof FlatList.sortRangeWithComparer, + "array", + (fun () -> + FlatList.sortRangeWithComparer Comparer.Default 0 0 defaultArray + |> ignore + )) + struct (nameof FlatList.sortWithComparer, + "array", + (fun () -> + FlatList.sortWithComparer Comparer.Default defaultArray + |> ignore + )) + struct (nameof FlatList.takeLast, "array", (fun () -> FlatList.takeLast 0 defaultArray |> ignore)) + struct (nameof FlatList.toBuilder, "array", (fun () -> FlatList.toBuilder defaultArray |> ignore)) + struct (nameof FlatList.toSeq, "array", (fun () -> FlatList.toSeq defaultArray |> ignore)) + struct (nameof FlatList.tryTail, "array", (fun () -> FlatList.tryTail defaultArray |> ignore)) + struct (nameof FlatList.update, "array", (fun () -> FlatList.update ignore defaultArray |> ignore)) + |] + |> Array.map (fun struct (name, paramName, actual) -> name, struct (paramName, actual)) + |> Map.ofArray + + /// The functions that take a sequence, by name and by the input that they reject, with the parameter that names the + /// input and the function on it. A default flat list converts to a sequence without a check and fails when the + /// sequence is enumerated, so the other inputs are valid and the function would enumerate it. + static let sequenceRejections = + let defaultOuter = Unchecked.defaultof>> + let nullItems = Unchecked.defaultof> + + // The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widenedItems = FlatListOracles.defaultStringsAsObjects + let objects = FlatList.ofArray [| "a" :> obj |] + + [| + struct ($"%s{nameof FlatList.concat} (default outer)", + "arrays", + (fun () -> FlatList.concat (defaultOuter :> seq<_>) |> ignore)) + struct ($"%s{nameof FlatList.transpose} (default outer)", + "arrays", + (fun () -> FlatList.transpose (defaultOuter :> seq<_>) |> ignore)) + struct ($"%s{nameof FlatList.insertManyAt} (default values)", + "values", + (fun () -> + FlatList.insertManyAt 1 (defaultArray :> seq<_>) (FlatList.ofArray Inputs.Several) + |> ignore + )) + // Array.insertManyAt checks the source before the values + struct ($"%s{nameof FlatList.insertManyAt} (default source and values)", + "source", + (fun () -> + FlatList.insertManyAt 1 (defaultArray :> seq<_>) defaultArray + |> ignore + )) + struct ($"%s{nameof FlatList.except} (default items)", + "itemsToExclude", + (fun () -> + FlatList.except (defaultArray :> seq<_>) (FlatList.ofArray Inputs.Several) + |> ignore + )) + // Array.except returns an empty input without enumerating the items, but it checks the items for null first + struct ($"%s{nameof FlatList.except} (default items, empty array)", + "itemsToExclude", + (fun () -> + FlatList.except (defaultArray :> seq<_>) FlatList.empty + |> ignore + )) + // Array.except checks the items to exclude before the array + struct ($"%s{nameof FlatList.except} (default items and array)", + "itemsToExclude", + (fun () -> + FlatList.except (defaultArray :> seq<_>) defaultArray + |> ignore + )) + struct ($"%s{nameof FlatList.removeMany} (default items)", + "items", + (fun () -> + FlatList.removeMany (defaultArray :> seq<_>) (FlatList.ofArray Inputs.Several) + |> ignore + )) + // ImmutableArray.RemoveRange checks the flat list before the items + struct ($"%s{nameof FlatList.removeMany} (default source and items)", + "source", + (fun () -> + FlatList.removeMany (defaultArray :> seq<_>) defaultArray + |> ignore + )) + struct ($"%s{nameof FlatList.removeMany} (null items)", + "items", + (fun () -> + FlatList.removeMany nullItems (FlatList.ofArray Inputs.Several) + |> ignore + )) + struct ($"%s{nameof FlatList.removeManyWith} (default items)", + "items", + (fun () -> + FlatList.removeManyWith + EqualityComparer.Default + (defaultArray :> seq<_>) + (FlatList.ofArray Inputs.Several) + |> ignore + )) + struct ($"%s{nameof FlatList.removeManyWith} (default source and items)", + "source", + (fun () -> + FlatList.removeManyWith EqualityComparer.Default (defaultArray :> seq<_>) defaultArray + |> ignore + )) + struct ($"%s{nameof FlatList.removeManyWith} (null items)", + "items", + (fun () -> + FlatList.removeManyWith EqualityComparer.Default nullItems (FlatList.ofArray Inputs.Several) + |> ignore + )) + struct ($"%s{nameof FlatList.ofSeq} (default flat list of strings as objects)", + "source", + (fun () -> FlatList.ofSeq widenedItems |> ignore)) + struct ($"%s{nameof FlatList.insertManyAt} (default flat list of strings as objects)", + "values", + (fun () -> FlatList.insertManyAt 1 widenedItems objects |> ignore)) + struct ($"%s{nameof FlatList.except} (default flat list of strings as objects)", + "itemsToExclude", + (fun () -> FlatList.except widenedItems objects |> ignore)) + struct ($"%s{nameof FlatList.removeMany} (default flat list of strings as objects)", + "items", + (fun () -> FlatList.removeMany widenedItems objects |> ignore)) + struct ($"%s{nameof FlatList.removeManyWith} (default flat list of strings as objects)", + "items", + (fun () -> + FlatList.removeManyWith EqualityComparer.Default widenedItems objects + |> ignore + )) + |] + |> Array.map (fun struct (name, paramName, actual) -> name, struct (paramName, actual)) + |> Map.ofArray + + /// The functions that take a sequence and mirror an Array function, by name, with that function on a null sequence and + /// the function on a null sequence. The other inputs are valid. + static let nullSequenceMirrors = + let nullArrays = Unchecked.defaultof> + let nullOuter = Unchecked.defaultof>> + let nullItems = Unchecked.defaultof> + + [| + struct (nameof FlatList.concat, + (fun () -> Array.concat nullArrays |> ignore), + (fun () -> FlatList.concat nullOuter |> ignore)) + struct (nameof FlatList.transpose, + (fun () -> Array.transpose nullArrays |> ignore), + (fun () -> FlatList.transpose nullOuter |> ignore)) + struct (nameof FlatList.except, + (fun () -> Array.except nullItems Inputs.Several |> ignore), + (fun () -> + FlatList.except nullItems (FlatList.ofArray Inputs.Several) + |> ignore + )) + struct (nameof FlatList.insertManyAt, + (fun () -> Array.insertManyAt 1 nullItems Inputs.Several |> ignore), + (fun () -> + FlatList.insertManyAt 1 nullItems (FlatList.ofArray Inputs.Several) + |> ignore + )) + |] + |> Array.map (fun struct (name, oracle, actual) -> name, struct (oracle, actual)) + |> Map.ofArray + + /// The functions that sort a range, by name, with the function and the sort of the range in the oracle. The inputs + /// have no two different elements with the same absolute value, and the comparer of `sortRangeWithComparer` orders by + /// the absolute value, so the result does not depend on the stability of the sort. + static let rangeSorts : Map int -> FlatList -> FlatList) * (int array -> int array))> = + Map.ofArray [| + nameof FlatList.sortRange, struct (FlatList.sortRange, Array.sortWith compare) + nameof FlatList.sortRangeWith, + struct (FlatList.sortRangeWith FlatListOracles.descending, Array.sortWith FlatListOracles.descending) + nameof FlatList.sortRangeWithComparer, + struct (FlatList.sortRangeWithComparer (Comparer.Create(Comparison FlatListOracles.byAbsolute)), + Array.sortWith FlatListOracles.byAbsolute) + |] + + /// The functions that take a range, by name, on the whole range of the input with several elements. + static let wholeRanges : Map -> FlatList> = + Map.ofArray [| + nameof FlatList.take, FlatList.take 8 + nameof FlatList.takeLast, FlatList.takeLast 8 + nameof FlatList.takeWhile, FlatList.takeWhile (fun _ -> true) + nameof FlatList.truncate, FlatList.truncate 100 + nameof FlatList.skip, FlatList.skip -1 + nameof FlatList.skipLast, FlatList.skipLast 0 + nameof FlatList.skipWhile, FlatList.skipWhile (fun _ -> false) + nameof FlatList.sub, (fun array -> FlatList.sub array 0 array.Length) + |] + + /// The functions that take a range, by name, on a partial range of the input with several elements. + static let partialRanges : Map -> FlatList> = + Map.ofArray [| + nameof FlatList.take, FlatList.take 3 + nameof FlatList.skip, FlatList.skip 3 + nameof FlatList.sub, (fun array -> FlatList.sub array 3 3) + ($"%s{nameof FlatList.splitAt} (first part)", + fun array -> + let struct (before, _) = FlatList.splitAt 3 array + before) + ($"%s{nameof FlatList.splitAt} (second part)", + fun array -> + let struct (_, after) = FlatList.splitAt 3 array + after) + |] + + /// Strings whose ordinal order differs from their order in the current culture. + static let strings () = [| "b"; "A"; "a"; "B" |] + + /// The names of the functions that mirror an Array function, as the rows of their test. + static member ArrayRejections = arrayRejections.Keys + + /// The names of the functions without an Array counterpart, as the rows of their test. + static member OtherRejections = otherRejections.Keys + + /// The names of the functions that take a sequence, as the rows of the test of the sequences that they reject. + static member SequenceRejections = sequenceRejections.Keys + + /// The names of the functions that take a sequence and mirror an Array function, as the rows of the test of a null sequence. + static member NullSequenceMirrors = nullSequenceMirrors.Keys + + /// Every range of every input, as the index of its first element and its length. + static member SubRanges = seq { + for input in Inputs.All do + for startIndex in 0 .. input.Length do + for count in 0 .. input.Length - startIndex do + struct (input, startIndex, count) + } + + /// Every input with ranges that Array.sub rejects: a negative start or count, a count beyond the length, and a start + /// beyond the length or at its end with an element to take. + static member SubRangesOutOfRange = seq { + for input in Inputs.All do + let ranges = + [| + struct (-1, 0) + struct (0, -1) + struct (-1, -1) + struct (0, input.Length + 1) + struct (input.Length + 1, 0) + struct (input.Length, 1) + struct (1, input.Length) + |] + |> Array.distinct + + for struct (startIndex, count) in ranges do + struct (input, startIndex, count) + } + + /// The ranges that the functions sort: none, one element, all of them, a prefix, a suffix and the inside of the input + /// with several elements, and the ranges of the smaller inputs. + static member SortRanges = seq { + let ranges = [| + struct (Inputs.Empty, 0, 0) + struct (Inputs.Singleton, 0, 0) + struct (Inputs.Singleton, 0, 1) + struct (Inputs.Singleton, 1, 0) + struct (Inputs.Several, 0, 0) + struct (Inputs.Several, 0, 1) + struct (Inputs.Several, 0, 8) + struct (Inputs.Several, 8, 0) + struct (Inputs.Several, 4, 0) + struct (Inputs.Several, 0, 3) + struct (Inputs.Several, 2, 4) + struct (Inputs.Several, 5, 3) + struct (Inputs.Several, 7, 1) + |] + + for name in rangeSorts.Keys do + for struct (input, index, count) in ranges do + struct (name, input, index, count) + } + + /// The ranges of the input with several elements that the sorts reject, with the parameter that the BCL names. + static member SortRangesOutOfRange = seq { + let ranges = [| + struct (-1, 1, "index") + struct (0, -1, "count") + struct (0, 9, "count") + struct (5, 4, "count") + struct (9, 0, "count") + |] + + for name in rangeSorts.Keys do + for struct (index, count, paramName) in ranges do + struct (name, index, count, paramName) + } + + /// The ranges of the input with several elements that need no sorting: they have at most one element. + static member SortRangesWithoutSorting = seq { + for name in rangeSorts.Keys do + for struct (index, count) in [| struct (0, 0); struct (2, 1); struct (8, 0) |] do + struct (name, index, count) + } + + /// Every input with each value to remove: the first of two equal elements, the only element, the last element, and + /// a value that no input has. + static member RemovalValues = seq { + for input in Inputs.All do + for value in [| 42; 5; -3; 8; 7; 1000 |] do + struct (input, value) + } + + /// Every input with values that none of them has. + static member AbsentValues = seq { + for input in Inputs.All do + for value in [| 1000; 99 |] do + struct (input, value) + } + + /// Every input with the values to remove: none, one, a value as often as the input with several elements has it and + /// more often, and several values of which some are missing. + static member RemovalItems = seq { + for input in Inputs.All do + for items in [| [||]; [| 5 |]; [| 5; 5 |]; [| 5; 5; 5 |]; [| -3; 5; 1000 |]; [| 7; 5; -3; 8; 5; 0 |] |] do + struct (input, items) + } + + /// Every input with sequences of values that none of them has. + static member AbsentItems = seq { + for input in Inputs.All do + for items in [| [||]; [| 1000 |]; [| 1000; 99 |] |] do + struct (input, items) + } + + /// The values to remove from the strings with a case-insensitive comparer: equal in the same case, equal in another + /// case, and missing. + static member NameValues = seq { + "a" + "b" + "B" + "C" + "d" + } + + /// The sequences of values to remove from the strings with a case-insensitive comparer. + static member NameItems : seq> = seq { + TestDataRow Array.empty + TestDataRow [| "B" |] + TestDataRow [| "b"; "B" |] + TestDataRow [| "B"; "b"; "b" |] + TestDataRow [| "C"; "d"; "A" |] + } + + /// Every input with an element, with each of its different elements: the value to replace. + static member ReplacedValues = seq { + for input in Inputs.NonEmpty do + for oldValue in Array.distinct input do + struct (input, oldValue) + } + + /// The values to replace in the strings with a case-insensitive comparer: equal in the same case and in another case. + static member ReplacedNames = seq { + "a" + "b" + "B" + "C" + } + + /// The names of the functions that take a range, as the rows of the test of the whole range. + static member WholeRanges = wholeRanges.Keys + + /// The names of the functions that take a range, as the rows of the test of a partial range. + static member PartialRanges = partialRanges.Keys + + /// Every index of the strings that binarySearch searches. + static member StringIndices = seq { 0 .. (strings ()).Length - 1 } + + [] + member _.``create repeats the value`` () = + CollectionAssert.AreEqual ([| 7; 7; 7 |], FlatList.toArray (FlatList.create 3 7), "FlatList.create must repeat the value") + Assert.IsEmpty (FlatList.create 0 7, "FlatList.create 0 must be empty") + + [] + member _.``create throws for a negative count as Array does`` () = + ExceptionAssert.Same ( + id, + (fun () -> Array.create -1 7 |> ignore), + (fun () -> FlatList.create -1 7 |> ignore), + "FlatList.create -1 must throw as Array.create does" + ) + + [] + member _.``isDefault and isDefaultOrEmpty tell a default value from an empty one`` () = + Assert.IsTrue (FlatList.isDefault defaultArray, "isDefault must be true for a default value") + Assert.IsFalse (FlatList.isDefault FlatList.empty, "isDefault must be false for an empty flat list") + Assert.IsTrue (FlatList.isDefaultOrEmpty defaultArray, "isDefaultOrEmpty must be true for a default value") + Assert.IsTrue (FlatList.isDefaultOrEmpty FlatList.empty, "isDefaultOrEmpty must be true for an empty flat list") + Assert.IsFalse ( + FlatList.isDefaultOrEmpty (FlatList.singleton 1), + "isDefaultOrEmpty must be false for a non-empty flat list" + ) + + [] + member _.``functions reject a default flat list as Array rejects a null array`` (name : string) = + let struct (oracle, actual) = arrayRejections[name] + ExceptionAssert.Same (id, oracle, actual, $"FlatList.%s{name} must reject a default flat list") + + [] + member _.``functions without an Array counterpart reject a default flat list`` (name : string) = + let struct (paramName, actual) = otherRejections[name] + FlatListAssert.RejectsNull ($"FlatList.%s{name}", paramName, actual) + + [] + member _.``functions that take a sequence reject a default flat list given as the sequence and a null sequence`` + (name : string) + = + let struct (paramName, actual) = sequenceRejections[name] + FlatListAssert.RejectsNull ($"FlatList.%s{name}", paramName, actual) + + [] + member _.``a flat list of strings that is not default passes as a sequence of objects`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + let asObjects (strings : string array) = strings |> Array.map (fun item -> item :> obj) + let objects = FlatList.ofArray (asObjects [| "c" |]) + + CollectionAssert.AreEqual ( + asObjects [| "a"; "b" |], + FlatList.toArray (FlatList.ofSeq widened), + "FlatList.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + asObjects [| "a"; "b"; "c" |], + FlatList.toArray (FlatList.insertManyAt 0 widened objects), + "FlatList.insertManyAt must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + asObjects [| "c" |], + FlatList.toArray (FlatList.except widened objects), + "FlatList.except must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + asObjects [| "c" |], + FlatList.toArray (FlatList.removeMany widened (FlatList.ofArray (asObjects [| "a"; "c"; "b" |]))), + "FlatList.removeMany must take the elements of a flat list of strings as objects" + ) + [] + member _.``functions that take a sequence reject a null sequence as Array does`` (name : string) = + let struct (oracle, actual) = nullSequenceMirrors[name] + + ExceptionAssert.Same ( + valuesParamName, + oracle, + actual, + $"FlatList.%s{name} must reject a null sequence as Array.%s{name} does" + ) + + [] + member _.``insertManyAt rejects an index out of range before the values as Array.insertManyAt does`` + (index : int, kind : string) + = + // The input has 8 elements, so the indices -1 and 9 are out of range. Array.insertManyAt checks the index + // before it enumerates the values, and the default flat list is as good as a null sequence for the oracle + let values = + match kind with + | "null" -> Unchecked.defaultof> + | _ -> defaultArray :> seq<_> + + ExceptionAssert.Same ( + id, + (fun () -> Array.insertManyAt index values Inputs.Several |> ignore), + (fun () -> + FlatList.insertManyAt index values (FlatList.ofArray Inputs.Several) + |> ignore + ), + "FlatList.insertManyAt must check the index before the values as Array.insertManyAt does" + ) + + [] + member _.``countBy counts null keys like any other key`` () = + let array = FlatList.ofArray Inputs.Several + let keyOf (item : int) = if item > 0 then Some (item % 2) else None + + CollectionAssert.AreEqual ( + Array.countBy keyOf Inputs.Several + |> Array.map (fun (key, count) -> struct (key, count)), + FlatList.countBy keyOf array |> FlatList.toArray, + "FlatList.countBy must count the key None, which is null, as Array.countBy does" + ) + + [] + member _.``asSpan views the elements`` () = + let array = FlatList.ofArray Inputs.Several + let span = FlatList.asSpan array + CollectionAssert.AreEqual (Inputs.Several, span.ToArray (), "FlatList.asSpan must view the elements") + + [] + member _.``asMemory views the underlying array without a copy`` () = + let array = FlatList.ofArray Inputs.Several + let memory = FlatList.asMemory array + let isArray, segment = MemoryMarshal.TryGetArray memory + Assert.IsTrue (isArray, "FlatList.asMemory must view an array") + Assert.AreSame (underlying array, segment.Array, "FlatList.asMemory must view the underlying array") + Assert.HasCount (Inputs.Several.Length, segment, "FlatList.asMemory must view every element") + + [] + member _.``binarySearch finds a value or the complement of its insertion point`` () = + let array = FlatList.ofArray [| 1; 3; 5; 7 |] + Assert.AreEqual(2, FlatList.binarySearch 5 array, "FlatList.binarySearch must find 5 at index 2") + Assert.AreEqual( + ~~~2, + FlatList.binarySearch 4 array, + "FlatList.binarySearch must return the complement of the insertion point of 4" + ) + Assert.AreEqual( + ~~~4, + FlatList.binarySearch 9 array, + "FlatList.binarySearch must return the complement of the length for 9" + ) + + [] + member _.``binarySearch uses the order of sort, which is ordinal for strings`` (index : int) = + let array = FlatList.ofArray (strings ()) |> FlatList.sort + CollectionAssert.AreEqual ([| "A"; "B"; "a"; "b" |], FlatList.toArray array, "FlatList.sort must order strings ordinally") + + Assert.AreEqual( + index, + FlatList.binarySearch array[index] array, + "FlatList.binarySearch must find every element of a sorted flat list at its index" + ) + + [] + member _.``blit copies a range into an array`` () = + let target = Array.zeroCreate 5 + FlatList.blit (FlatList.ofArray Inputs.Several) 2 target 1 3 + CollectionAssert.AreEqual ([| 0; 8; 5; 0; 0 |], target, "FlatList.blit must copy three elements from index 2 to index 1") + + [] + member _.``blit throws for a range out of range as Array does`` () = + let source = Inputs.Several + + ExceptionAssert.Same ( + id, + (fun () -> Array.blit source 6 (Array.zeroCreate 5) 0 3), + (fun () -> FlatList.blit (FlatList.ofArray source) 6 (Array.zeroCreate 5) 0 3), + "FlatList.blit beyond the source must throw as Array.blit does" + ) + + [] + member _.``ofSeq of a flat list shares its storage`` () = + let array = FlatList.ofArray Inputs.Several + Assert.AreSame (underlying array, underlying (FlatList.ofSeq array), "FlatList.ofSeq of a flat list must not copy it") + + [] + member _.``range functions return the input itself for the whole range`` (name : string) = + let array = FlatList.ofArray Inputs.Several + + Assert.AreSame ( + underlying array, + underlying (wholeRanges[name] array), + $"FlatList.%s{name} of the whole range must return the input" + ) + + [] + member _.``range functions copy a partial range`` (name : string) = + let array = FlatList.ofArray Inputs.Several + + Assert.AreNotSame ( + underlying array, + underlying (partialRanges[name] array), + $"FlatList.%s{name} of a partial range must copy it" + ) + + [] + member _.``append returns the other flat list when one is empty`` () = + let array = FlatList.ofArray Inputs.Several + Assert.AreSame ( + underlying array, + underlying (FlatList.append array FlatList.empty), + "FlatList.append of an empty second flat list must return the first one" + ) + Assert.AreSame ( + underlying array, + underlying (FlatList.append FlatList.empty array), + "FlatList.append of an empty first flat list must return the second one" + ) + + [] + member _.``sortWithComparer returns a sorted input itself`` () = + let array = FlatList.ofArray [| 1; 2; 3 |] + Assert.AreSame ( + underlying array, + underlying (FlatList.sortWithComparer Comparer.Default array), + "FlatList.sortWithComparer must not copy a sorted flat list" + ) + + // ---- sub ---- + + [] + member _.``sub returns the range of the elements as Array.sub does`` (input : int array, startIndex : int, count : int) = + CollectionAssert.AreEqual ( + Array.sub input startIndex count, + FlatList.toArray (FlatList.sub (FlatList.ofArray input) startIndex count), + "FlatList.sub must return the range of the elements as Array.sub does" + ) + + [] + member _.``sub throws for a range out of range as Array.sub does`` (input : int array, startIndex : int, count : int) = + let oracle () = Array.sub input startIndex count |> ignore + + let actual () = + FlatList.sub (FlatList.ofArray input) startIndex count + |> ignore + + ExceptionAssert.Same (id, oracle, actual, "FlatList.sub must throw as Array.sub does for a range out of range") + + // ExceptionAssert compares the first line of the messages. The second line names the argument and, for a range + // beyond the end, the length: "count = 9, array.Length = 8" + Assert.AreEqual( + FlatListOracles.messageDetail oracle, + FlatListOracles.messageDetail actual, + "FlatList.sub must write the arguments of the message as Array.sub does" + ) + + [] + member _.``sub rejects a default flat list before the range as Array.sub rejects a null array`` + (startIndex : int, count : int) + = + ExceptionAssert.Same ( + id, + (fun () -> Array.sub nullArray startIndex count |> ignore), + (fun () -> FlatList.sub defaultArray startIndex count |> ignore), + "FlatList.sub must reject a default flat list as Array.sub rejects a null array" + ) + + [] + member _.``sub returns the empty flat list for an empty range`` (startIndex : int) = + let array = FlatList.ofArray Inputs.Several + + Assert.AreSame ( + underlying FlatList.empty, + underlying (FlatList.sub array startIndex 0), + "FlatList.sub of an empty range must return the empty flat list" + ) + + // ---- sortRange, sortRangeWith and sortRangeWithComparer ---- + + [] + member _.``functions that sort a range sort the range and keep the other elements`` + (name : string, input : int array, index : int, count : int) + = + let struct (sortRange, sortPart) = rangeSorts[name] + + let expected = + Array.concat [| + Array.sub input 0 index + sortPart (Array.sub input index count) + Array.sub input (index + count) (input.Length - index - count) + |] + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray (sortRange index count (FlatList.ofArray input)), + $"FlatList.%s{name} must sort the range and keep the other elements" + ) + + [] + member _.``functions that sort a range throw for a range out of range as the BCL does`` + (name : string, index : int, count : int, paramName : string) + = + let struct (sortRange, _) = rangeSorts[name] + + let ex = + Assert.ThrowsExactly( + Action (fun () -> + sortRange index count (FlatList.ofArray Inputs.Several) + |> ignore + ), + $"FlatList.%s{name} must throw ArgumentOutOfRangeException for a range out of range" + ) + + Assert.AreEqual(paramName, ex.ParamName, $"FlatList.%s{name} must name the parameter that the BCL names") + + [] + member _.``functions that sort a range return the input itself for a range of at most one element`` + (name : string, index : int, count : int) + = + let struct (sortRange, _) = rangeSorts[name] + let array = FlatList.ofArray Inputs.Several + + Assert.AreSame ( + underlying array, + underlying (sortRange index count array), + $"FlatList.%s{name} must not copy a range that needs no sorting" + ) + + [] + member _.``sortRange returns the input itself when the range is already sorted`` () = + let array = FlatList.ofArray [| 1; 2; 3; 9; 5 |] + + Assert.AreSame ( + underlying array, + underlying (FlatList.sortRange 0 3 array), + "FlatList.sortRange must not copy a range that is already sorted" + ) + + [] + member _.``sortRange orders strings ordinally, as F# generic comparison does`` () = + let array = FlatList.ofArray (strings ()) + + CollectionAssert.AreEqual ( + [| "A"; "B"; "a"; "b" |], + FlatList.toArray (FlatList.sortRange 0 4 array), + "FlatList.sortRange must sort strings ordinally" + ) + + // ---- remove, removeWith, removeMany and removeManyWith ---- + + [] + member _.``remove removes the first equal element`` (input : int array, value : int) = + CollectionAssert.AreEqual ( + FlatListOracles.removeFirst (=) value input, + FlatList.toArray (FlatList.remove value (FlatList.ofArray input)), + "FlatList.remove must remove the first equal element" + ) + + [] + member _.``remove returns the input itself when no element equals the value`` (input : int array, value : int) = + let array = FlatList.ofArray input + + Assert.AreSame ( + underlying array, + underlying (FlatList.remove value array), + "FlatList.remove must not copy a flat list that has no equal element" + ) + + [] + member _.``remove compares in the default equality, which tells b from B`` () = + CollectionAssert.AreEqual ( + [| "a"; "b"; "c" |], + FlatList.toArray (FlatList.remove "B" (FlatList.ofArray (FlatListOracles.names ()))), + "FlatList.remove must not find B equal to b" + ) + + [] + member _.``removeWith removes the first element that the comparer finds equal`` (value : string) = + CollectionAssert.AreEqual ( + FlatListOracles.removeFirst FlatListOracles.equalIgnoringCase value (FlatListOracles.names ()), + FlatList.toArray ( + FlatList.removeWith StringComparer.OrdinalIgnoreCase value (FlatList.ofArray (FlatListOracles.names ())) + ), + "FlatList.removeWith must remove the first element that the comparer finds equal" + ) + + [] + member _.``removeWith returns the input itself when no element is equal`` (value : string) = + let array = FlatList.ofArray (FlatListOracles.names ()) + + Assert.AreSame ( + underlying array, + underlying (FlatList.removeWith StringComparer.OrdinalIgnoreCase value array), + "FlatList.removeWith must not copy a flat list that has no equal element" + ) + + [] + member _.``removeMany removes one element for each value`` (input : int array, items : int array) = + let expected = + Array.fold (fun elements item -> FlatListOracles.removeFirst (=) item elements) input items + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray (FlatList.removeMany items (FlatList.ofArray input)), + "FlatList.removeMany must remove one element for each value" + ) + + [] + member _.``removeMany takes the values as any sequence`` () = + let array = FlatList.ofArray Inputs.Several + // The values 5, -3 and 5 remove the first 5, the first -3 and the second 5 + let expected = [| 8; 0; -3; 12; 7 |] + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray (FlatList.removeMany [| 5; -3; 5 |] array), + "FlatList.removeMany must take the values as an array" + ) + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray (FlatList.removeMany [ 5; -3; 5 ] array), + "FlatList.removeMany must take the values as a list" + ) + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray (FlatList.removeMany (Seq.map id [| 5; -3; 5 |]) array), + "FlatList.removeMany must take the values as a lazy sequence" + ) + + [] + member _.``removeMany returns the input itself when no value is in it`` (input : int array, items : int array) = + let array = FlatList.ofArray input + + Assert.AreSame ( + underlying array, + underlying (FlatList.removeMany items array), + "FlatList.removeMany must not copy a flat list that has none of the values" + ) + + [] + member _.``removeMany compares in the default equality, which tells b from B`` () = + CollectionAssert.AreEqual ( + [| "a"; "b"; "c" |], + FlatList.toArray (FlatList.removeMany [| "B" |] (FlatList.ofArray (FlatListOracles.names ()))), + "FlatList.removeMany must not find B equal to b" + ) + + [] + member _.``removeManyWith removes one element for each value that the comparer finds equal`` (items : string array) = + let expected = + Array.fold + (fun elements item -> FlatListOracles.removeFirst FlatListOracles.equalIgnoringCase item elements) + (FlatListOracles.names ()) + items + + CollectionAssert.AreEqual ( + expected, + FlatList.toArray ( + FlatList.removeManyWith StringComparer.OrdinalIgnoreCase items (FlatList.ofArray (FlatListOracles.names ())) + ), + "FlatList.removeManyWith must remove one element for each value that the comparer finds equal" + ) + + // ---- replace and replaceWith ---- + + [] + member _.``replace replaces the first equal element`` (input : int array, oldValue : int) = + CollectionAssert.AreEqual ( + FlatListOracles.replaceFirst (=) oldValue 99 input + |> ValueOption.get, + FlatList.toArray (FlatList.replace oldValue 99 (FlatList.ofArray input)), + "FlatList.replace must replace the first equal element" + ) + + [] + member _.``replace throws for a value that no element equals as the BCL does`` (input : int array, value : int) = + ExceptionAssert.Same ( + id, + (fun () -> (FlatList.ofArray input).Replace(value, 0) |> ignore), + (fun () -> FlatList.replace value 0 (FlatList.ofArray input) |> ignore), + "FlatList.replace must throw as ImmutableArray.Replace does when no element equals the value" + ) + + [] + member _.``replace returns a new flat list also when the new value equals the old one`` (value : int) = + let array = FlatList.ofArray Inputs.Several + let replaced = FlatList.replace value value array + + CollectionAssert.AreEqual (Inputs.Several, FlatList.toArray replaced, "FlatList.replace must keep the elements") + + Assert.AreNotSame ( + underlying array, + underlying replaced, + "FlatList.replace must return a new flat list, as ImmutableArray.Replace does" + ) + + [] + member _.``replace compares in the default equality, which tells b from B`` () = + CollectionAssert.AreEqual ( + [| "a"; "b"; "X"; "c" |], + FlatList.toArray (FlatList.replace "B" "X" (FlatList.ofArray (FlatListOracles.names ()))), + "FlatList.replace must not find B equal to b" + ) + + [] + member _.``replaceWith replaces the first element that the comparer finds equal`` (oldValue : string) = + let names = FlatListOracles.names () + + CollectionAssert.AreEqual ( + FlatListOracles.replaceFirst FlatListOracles.equalIgnoringCase oldValue "X" names + |> ValueOption.get, + FlatList.toArray (FlatList.replaceWith StringComparer.OrdinalIgnoreCase oldValue "X" (FlatList.ofArray names)), + "FlatList.replaceWith must replace the first element that the comparer finds equal" + ) + + [] + member _.``replaceWith throws for a value that no element equals as the BCL does`` (oldValue : string) = + let array = FlatList.ofArray (FlatListOracles.names ()) + + ExceptionAssert.Same ( + id, + (fun () -> + array.Replace (oldValue, "X", StringComparer.OrdinalIgnoreCase) + |> ignore + ), + (fun () -> + FlatList.replaceWith StringComparer.OrdinalIgnoreCase oldValue "X" array + |> ignore + ), + "FlatList.replaceWith must throw as ImmutableArray.Replace does when no element equals the value" + ) + + [] + member _.``replaceWith takes null for the default comparer`` () = + let comparer = Unchecked.defaultof> + + CollectionAssert.AreEqual ( + [| "a"; "b"; "X"; "c" |], + FlatList.toArray (FlatList.replaceWith comparer "B" "X" (FlatList.ofArray (FlatListOracles.names ()))), + "FlatList.replaceWith must compare in the default equality for a null comparer" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTupleTests.fs new file mode 100644 index 00000000..2b0a34a2 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/FlatList/FlatListTupleTests.fs @@ -0,0 +1,223 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Linq +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The type annotations of the results are the proof that the functions of the Tuple namespace shadow the default ones. + +/// The `FlatList` module of the `Tuple` namespace. +[] +type FlatListTupleTests () = + + static let array = FlatList.ofArray Inputs.Several + + static let other = FlatList.ofArray Inputs.Other + + static let third = FlatList.ofArray Inputs.Third + + [] + member _.``unfold takes a generator of value options of reference tuples`` () = + let generator (state : int) = + if state > 0 then + ValueSome (state, state - 1) + else + ValueNone + let generated : FlatList = FlatList.unfold generator 3 + CollectionAssert.AreEqual ([| 3; 2; 1 |], FlatList.toArray generated, "Tuple.FlatList.unfold must generate the elements") + + [] + member _.``allPairs returns reference tuples`` () = + let pairs : FlatList = FlatList.allPairs array other + + CollectionAssert.AreEqual ( + Array.allPairs Inputs.Several Inputs.Other, + FlatList.toArray pairs, + "Tuple.FlatList.allPairs must pair as Array.allPairs does" + ) + + [] + member _.``indexed returns reference tuples`` () = + let pairs : FlatList = FlatList.indexed array + CollectionAssert.AreEqual ( + Array.indexed Inputs.Several, + FlatList.toArray pairs, + "Tuple.FlatList.indexed must index as Array.indexed does" + ) + + [] + member _.``pairwise returns reference tuples`` () = + let pairs : FlatList = FlatList.pairwise array + + CollectionAssert.AreEqual ( + Array.pairwise Inputs.Several, + FlatList.toArray pairs, + "Tuple.FlatList.pairwise must pair as Array.pairwise does" + ) + + [] + member _.``unzip takes and returns reference tuples`` () = + let (first : FlatList), (second : FlatList) = FlatList.unzip (FlatList.zip array other) + CollectionAssert.AreEqual ( + Inputs.Several, + FlatList.toArray first, + "Tuple.FlatList.unzip must return the first components" + ) + CollectionAssert.AreEqual ( + Inputs.Other, + FlatList.toArray second, + "Tuple.FlatList.unzip must return the second components" + ) + + [] + member _.``unzip3 takes and returns reference tuples`` () = + let (first : FlatList), (second : FlatList), (last : FlatList) = + FlatList.unzip3 (FlatList.zip3 array other third) + CollectionAssert.AreEqual ( + Inputs.Several, + FlatList.toArray first, + "Tuple.FlatList.unzip3 must return the first components" + ) + CollectionAssert.AreEqual ( + Inputs.Other, + FlatList.toArray second, + "Tuple.FlatList.unzip3 must return the second components" + ) + CollectionAssert.AreEqual (Inputs.Third, FlatList.toArray last, "Tuple.FlatList.unzip3 must return the third components") + + [] + member _.``zip returns reference tuples`` () = + let pairs : FlatList = FlatList.zip array other + CollectionAssert.AreEqual ( + Array.zip Inputs.Several Inputs.Other, + FlatList.toArray pairs, + "Tuple.FlatList.zip must zip as Array.zip does" + ) + + [] + member _.``zip throws for different lengths as the default does`` () = + ExceptionAssert.Same ( + id, + (fun () -> + FSharp.Collections.Immutable.FlatList.zip array (FlatList.singleton 1) + |> ignore + ), + (fun () -> FlatList.zip array (FlatList.singleton 1) |> ignore), + "Tuple.FlatList.zip must throw as the default zip does" + ) + + [] + member _.``zip3 returns reference tuples`` () = + let triples : FlatList = FlatList.zip3 array other third + + CollectionAssert.AreEqual ( + Array.zip3 Inputs.Several Inputs.Other Inputs.Third, + FlatList.toArray triples, + "Tuple.FlatList.zip3 must zip as Array.zip3 does" + ) + + [] + member _.``mapFold takes and returns reference tuples`` () = + let (results : FlatList), (state : int) = + FlatList.mapFold (fun state item -> item * state, state + 1) 1 array + let expectedResults, expectedState = + Array.mapFold (fun state item -> item * state, state + 1) 1 Inputs.Several + CollectionAssert.AreEqual ( + expectedResults, + FlatList.toArray results, + "Tuple.FlatList.mapFold must map as Array.mapFold does" + ) + Assert.AreEqual(expectedState, state, "Tuple.FlatList.mapFold must thread the state as Array.mapFold does") + + [] + member _.``mapFoldBack takes and returns reference tuples`` () = + let (results : FlatList), (state : int) = + FlatList.mapFoldBack (fun item state -> item * state, state + 1) array 1 + let expectedResults, expectedState = + Array.mapFoldBack (fun item state -> item * state, state + 1) Inputs.Several 1 + CollectionAssert.AreEqual ( + expectedResults, + FlatList.toArray results, + "Tuple.FlatList.mapFoldBack must map as Array.mapFoldBack does" + ) + Assert.AreEqual(expectedState, state, "Tuple.FlatList.mapFoldBack must thread the state as Array.mapFoldBack does") + + [] + member _.``countBy returns reference tuples`` () = + let counts : FlatList = FlatList.countBy (fun item -> item % 3) array + + CollectionAssert.AreEqual ( + Array.countBy (fun item -> item % 3) Inputs.Several, + FlatList.toArray counts, + "Tuple.FlatList.countBy must count as Array.countBy does" + ) + + [] + member _.``groupBy returns reference tuples`` () = + let groups : FlatList> = FlatList.groupBy (fun item -> item % 3) array + let expected = Array.groupBy (fun item -> item % 3) Inputs.Several + + // A group pairs its key with its elements, which a pair compares by reference, so the keys and the elements are + // compared apart + CollectionAssert.AreEqual ( + Array.map fst expected, + groups |> _.Select(fst) |> Seq.toArray, + "Tuple.FlatList.groupBy must return the keys as Array.groupBy does" + ) + + CollectionAssert.AreEqual ( + Array.map snd expected, + groups + |> _.Select(fun (_, items) -> FlatList.toArray items) + |> Seq.toArray, + "Tuple.FlatList.groupBy must group the elements as Array.groupBy does" + ) + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : FlatList), (rejected : FlatList) = + FlatList.partition (fun item -> item > 0) array + let expectedAccepted, expectedRejected = Array.partition (fun item -> item > 0) Inputs.Several + CollectionAssert.AreEqual ( + expectedAccepted, + FlatList.toArray accepted, + "Tuple.FlatList.partition must return the accepted elements" + ) + CollectionAssert.AreEqual ( + expectedRejected, + FlatList.toArray rejected, + "Tuple.FlatList.partition must return the rejected elements" + ) + + [] + member _.``partitionWith returns a reference tuple`` () = + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + let (first : FlatList), (second : FlatList) = FlatList.partitionWith partitioner array + let expectedFirst, expectedSecond = Array.partitionWith partitioner Inputs.Several + CollectionAssert.AreEqual ( + expectedFirst, + FlatList.toArray first, + "Tuple.FlatList.partitionWith must return the first choices" + ) + CollectionAssert.AreEqual ( + expectedSecond, + FlatList.toArray second, + "Tuple.FlatList.partitionWith must return the second choices" + ) + + [] + member _.``splitAt returns a reference tuple`` () = + let (before : FlatList), (after : FlatList) = FlatList.splitAt 3 array + let expectedBefore, expectedAfter = Array.splitAt 3 Inputs.Several + CollectionAssert.AreEqual (expectedBefore, FlatList.toArray before, "Tuple.FlatList.splitAt must return the first part") + CollectionAssert.AreEqual (expectedAfter, FlatList.toArray after, "Tuple.FlatList.splitAt must return the second part") + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = FlatList.tryFind (fun item -> item > 6) array + Assert.AreEqual(ValueSome 8, found, "FlatList.tryFind must resolve to the default module")