diff --git a/CHANGELOG.md b/CHANGELOG.md index 8373ad55..e156a682 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 semantics and exceptions. Its `tryFind`, `tryFindIndex` and `tryPick` return `voption`, `choose` and `tryPick` take functions that return `voption`, and `groupBy`, `partition`, `partitionWith` and `zip` return struct tuples; the `Parallel` modules of the `Option` and `Tuple` namespaces repeat them with options and reference tuples. +* `HashSet` has every function of the FSharp.Core `Set` module that applies to a hash set, with its semantics, argument + order and exceptions, plus `addRange`, `build`, `builder`, `choose`, `clear`, `comparer`, `forComparer`, `ofBuilder`, + `ofReadOnlySpan`, `ofSpan`, `overlaps`, `removeRange`, `setEquals`, `symmetricDifference`, `toBuilder`, `tryGetValue` + and `update`. +* `HashSet.addRange elements set` and `removeRange elements set` wrap the `Union` and `Except` members that take a + sequence. The result keeps the comparer of the hash set, an element of the sequence that the comparer finds equal to + an element of the hash set does not replace it, and the input itself comes back when nothing changes. +* `HashSet.ofReadOnlySpan` and `ofSpan` create a hash set from a span, `build` fills a builder with an action, and + `update` applies an action to a builder of a hash set and returns its contents, so that the in-place operations of the + builder, such as `IntersectWith`, are one expression. +* `HashSet.forComparer comparer` creates the `CustomComparerHashSet` struct, whose members `build`, `builder`, `empty`, + `ofArray`, `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSpan` and `singleton` create hash sets and builders that compare + with the comparer, as in `(HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names`. They replace the BCL + members `Create(comparer, ...)`, `CreateRange(comparer, items)`, `ToImmutableHashSet(items, comparer)` and + `CreateBuilder(comparer)`. `ofSeq` also gives an existing hash set the comparer, as `WithComparer` does: the hash set + itself comes back for the same comparer instance, and an equal but distinct instance rebuilds it. + `WithComparer(null)` is `HashSet.ofSeq set`. A `null` comparer throws `ArgumentNullException`. +* `HashSet.add` and `remove` return the input itself when nothing changes, so a reference comparison of the input and + the result tells whether `Add` changed anything, as the `bool` result of `Builder.Add` does. Hash sets have no builder + module: `builder`, `toBuilder` and `ofBuilder` are `CreateBuilder`, `ToBuilder` and `ToImmutable`, and the builder is + used directly. The mutating members of the BCL interfaces, such as `ISet.UnionWith` and `ICollection.Add`, + throw `NotSupportedException` on a hash set and have no function. +* `add`, `addRange`, `remove`, `removeRange`, `clear`, `update`, `filter` and `partition` of `HashSet` keep the comparer + of their input, and the set algebra keeps the comparer of the first hash set; `map`, `choose` and `partitionWith` + compare with the default comparer of the result type. Every function throws `ArgumentNullException` for a `null` hash + set, sequence or array, and for a default flat list that is given as a sequence; `yield!` and `for` of + `hashSet { }` do the same. `unionMany` of an empty sequence returns an empty hash set, as `Set.unionMany` does. + `tryGetValue` returns a `voption`, `choose` takes a function that returns a `voption`, and `partition` and + `partitionWith` return struct tuples. +* The set algebra and the relations of `HashSet` take hash sets, as the `Set` functions do. The BCL members that take a + sequence map as follows. `Union` and `Except` are `addRange` and `removeRange`, and `Overlaps` and `IsSupersetOf` are + `Seq.exists` and `Seq.forall` with `HashSet.contains`, which compare in the comparer of the hash set for every + comparer. To pass a sequence to `Intersect`, `SymmetricExcept`, `IsSubsetOf`, `IsProperSubsetOf`, + `IsProperSupersetOf` or `SetEquals`, convert it with `(HashSet.forComparer (HashSet.comparer set)).ofSeq elements`: + the elements get the comparer of the first hash set, as they do in the members. `HashSet.ofSeq elements` converts + them with the default comparer instead, which merges the elements that it finds equal before the function sees them. + The answer is that of the member when the comparer of `set` finds equal every pair of elements that the default + comparer finds equal, as the default comparer and `StringComparer.OrdinalIgnoreCase` do. It can differ when the + comparer of `set` tells such elements apart, as `HashIdentity.Reference` does for two strings with the same text. +* The element that stays when the comparer finds two elements equal follows the BCL: `union` and `addRange` keep the + element of the first hash set, `intersect` keeps the elements of the second one, and the creation functions keep the + first element of their input. `isProperSuperset` is the exception: it counts the elements of the second hash set that + the comparer of the first one finds equal once, as the documentation of `IsProperSupersetOf` says, where the member + counts each of them. +* The `ISet` module works on any `IImmutableSet<'T>` with the functions of `HashSet` that need neither a creation nor a + comparer, `addRange` and `removeRange` included; the transformations that keep the element type return the + implementation of their input. The interface cannot tell its comparer, so a sequence for a relation, `intersect` or + `symmetricDifference` is converted with `ISet.addRange elements (ISet.clear set)`, which has the implementation and + the comparer of `set`, where `HashSet` uses `(HashSet.forComparer (HashSet.comparer set)).ofSeq elements`. +* The `FSharp.Collections.Immutable.Option.HashSet`, `FSharp.Collections.Immutable.Tuple.HashSet`, + `FSharp.Collections.Immutable.Option.ISet` and `FSharp.Collections.Immutable.Tuple.ISet` modules repeat `choose` and + `tryGetValue` with options, and `partition` and `partitionWith` with reference tuples. +* `hashSet { }` builds a hash set, and `HashSetCE` created with an equality comparer builds hash sets that compare with it; + a `null` comparer throws `ArgumentNullException`. ### Changed @@ -61,6 +115,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 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`. +* The `ISet<'T>` abbreviation of `IImmutableSet<'T>`, because it shadowed `System.Collections.Generic.ISet<'T>`: use + `IImmutableSet<'T>` and the `ISet` module. ## [1.0.0] - 2020-02-23 diff --git a/README.md b/README.md index 950057e1..555b4c97 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,9 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | -| `HashSet` | `ImmutableHashSet<'T>` | – | +| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | [Documentation](https://fsprojects.github.io/FSharp.Collections.Immutable/) diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index 822820fa..88b0f21a 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -32,8 +32,9 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | -| `HashSet` | `ImmutableHashSet<'T>` | – | +| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | --- diff --git a/docsSrc/index.md b/docsSrc/index.md index 276ad8fe..7541de20 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -14,8 +14,9 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | -| `HashSet` | `ImmutableHashSet<'T>` | – | +| `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | | `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | --- diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 6db0b059..156d4eb5 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fs +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -4,3 +4,6 @@ module FSharp.Collections.Immutable.ComputationExpressions // ---- Creation ---- let flatList = FlatListCE () + +[] +let hashSet<'T> = HashSetCE<'T>() diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi index c0086e56..78396635 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -19,3 +19,30 @@ module FSharp.Collections.Immutable.ComputationExpressions /// Evaluates to a flat list of 0, 1, 4, 9, 100 and 200. /// val flatList : FlatListCE + +/// +/// Builds a hash set that compares with . +/// +/// +/// The value is a type function, so that one value builds hash sets of every element type. A +/// created with an equality comparer builds hash sets that +/// compare with it. +/// +/// +/// +/// let squares = hashSet { +/// for i in -2..2 do +/// yield i * i +/// yield! [ 4; 9 ] +/// } +/// +/// Evaluates to a hash set of 0, 1, 4 and 9. +/// +/// +/// +/// let names = HashSetCE StringComparer.OrdinalIgnoreCase { "Ann"; "ANN"; "Bob" } +/// +/// Evaluates to a hash set of "Ann" and "Bob" that ignores case. +/// +[] +val hashSet<'T> : HashSetCE<'T> diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 293de773..ee70cb54 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -33,6 +33,8 @@ + + @@ -44,6 +46,27 @@ + + + HashSet.fsi + + + HashSet.fs + + + + + + + + + + + + + + + diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fs b/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fs new file mode 100644 index 00000000..0d468ba1 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fs @@ -0,0 +1,78 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable +// Opened after System.Collections.Generic, so that HashSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +// A struct with a primary constructor cannot check its argument, so the field and the constructor are explicit (ADR 0001, +// D12). The field is null only in the default value of the struct, which no constructor creates; the BCL treats a null +// comparer as the default comparer, and the Comparer property says so. +[] +type CustomComparerHashSet<'T> = + + val private comparer : IEqualityComparer<'T> | null + + new (comparer : IEqualityComparer<'T>) + = + checkNotNull (nameof comparer) comparer + { comparer = comparer } + + // ---- Creation ---- + + [] + member this.empty : HashSet<'T> = ImmutableHashSet.Create<'T> this.Comparer + + [] + member this.ofArray (array : 'T array) : HashSet<'T> = + checkNotNull (nameof array) array + ImmutableHashSet.CreateRange<'T>(this.Comparer, array) + + [] + member this.ofList (elements : 'T list) : HashSet<'T> = + // Set.ofList dereferences a null list without a check; a null input throws ArgumentNullException here (D6) + checkNotNull (nameof elements) elements + let builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer + + for item in elements do + builder.Add item |> ignore + + builder.ToImmutable () + + // The elements of the span are added in order, so the first of the elements that the comparer finds equal stays + [] + member this.ofReadOnlySpan (span : ReadOnlySpan<'T>) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, span) + + [] + member this.ofSeq (elements : seq<'T>) : HashSet<'T> = + // A default flat list given as a sequence would fail with InvalidOperationException in the BCL call (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof elements) elements + // A hash set gets the comparer through WithComparer, which returns the hash set itself for the same comparer + // instance (the test is reference equality, ADR 0001, D14) + ImmutableHashSet.CreateRange<'T>(this.Comparer, elements) + + // Create takes a read-only span; F# converts the span of the argument + [] + member this.ofSpan (span : Span<'T>) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, span) + + [] + member this.singleton (value : 'T) : HashSet<'T> = ImmutableHashSet.Create<'T>(this.Comparer, value) + + // ---- Builder ---- + + [] + member this.build (action : HashSet<'T>.Builder -> unit) : HashSet<'T> = + let builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer + action builder + builder.ToImmutable () + + [] + member this.builder () : HashSet<'T>.Builder = ImmutableHashSet.CreateBuilder<'T> this.Comparer + + // ---- Properties ---- + + member this.Comparer : IEqualityComparer<'T> = + match this.comparer with + | null -> EqualityComparer<'T>.Default :> IEqualityComparer<'T> + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fsi b/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fsi new file mode 100644 index 00000000..aa2298c8 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSet.CustomComparer.fsi @@ -0,0 +1,184 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +// Opened after System.Collections.Generic, so that HashSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +/// +/// Creates hash sets that compare their elements with an equality comparer (ADR 0001, D12). +/// +/// +/// +/// +/// creates the struct. Its members are the creation functions of +/// under the same names, with the comparer applied, so that a call reads as one phrase. +/// +/// +/// The struct holds only the comparer, so creating it allocates nothing. Its default value, which no constructor +/// creates, compares with . +/// +/// +/// +/// +/// let names = (HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofList [ "Ann"; "ANN"; "Bob" ] +/// +/// Evaluates to a hash set of "Ann" and "Bob" that compares without case. +/// +[] +type CustomComparerHashSet<'T> = + + // A signature reveals the fields of a struct, private ones included (FS0314); null only in the default value + val private comparer : IEqualityComparer<'T> | null + + /// Creates the struct for an equality comparer. + /// The equality comparer of the elements of the hash sets that the struct creates. + /// Thrown when is . + new : comparer : IEqualityComparer<'T> -> CustomComparerHashSet<'T> + + // ---- Creation ---- + + /// + /// The empty hash set that compares with the comparer. + /// + /// + /// O(1). The member wraps + /// . + /// + [] + member empty : HashSet<'T> + + /// + /// Creates a hash set from the elements of an array, compared with the comparer. + /// + /// The input array. + /// + /// The hash set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is . + /// + /// O(n log n). Later changes to the array do not change the hash set. The member wraps + /// . + /// + [] + member ofArray : array : 'T array -> HashSet<'T> + + /// + /// Creates a hash set from the elements of a list, compared with the comparer. + /// + /// The input list. + /// + /// The hash set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is . + /// + /// O(n log n). + /// + [] + member ofList : elements : 'T list -> HashSet<'T> + + /// + /// Creates a hash set from the elements of a read-only span, compared with the comparer. + /// + /// The input span. + /// + /// The hash set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. An empty span gives the empty hash set. + /// + /// + /// O(n log n). Later changes to the memory under the span do not change the hash set. The member wraps + /// . + /// + [] + member ofReadOnlySpan : span : ReadOnlySpan<'T> -> HashSet<'T> + + /// + /// Creates a hash set from the elements of a sequence, compared with the comparer. + /// + /// The input sequence, which can be a hash set with another comparer. + /// + /// The hash set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. + /// + /// Thrown when is or a default flat list. + /// + /// + /// O(n log n). The member wraps + /// . + /// + /// + /// A hash set gets the comparer, as + /// + /// gives it: the member returns the hash set itself in O(1) when its comparer is the same instance as the comparer of + /// the struct, and otherwise a new hash set of its elements in the comparer. A comparer that is equal to the comparer + /// of the struct but is another instance does not count (ADR 0001, D14). + /// + /// + [] + member ofSeq : elements : seq<'T> -> HashSet<'T> + + /// + /// Creates a hash set from the elements of a span, compared with the comparer. + /// + /// The input span. + /// + /// The hash set of the elements that are distinct in the comparer; of the elements that the comparer considers equal, + /// it holds the first one. An empty span gives the empty hash set. + /// + /// + /// O(n log n). Later changes to the memory under the span do not change the hash set. The member converts the span to a + /// read-only span and wraps + /// . + /// + [] + member ofSpan : span : Span<'T> -> HashSet<'T> + + /// + /// Creates a hash set of one element that compares with the comparer. + /// + /// The element. + /// The hash set of one element. + /// + /// O(1). The member wraps + /// . + /// + [] + member singleton : value : 'T -> HashSet<'T> + + // ---- Builder ---- + + /// + /// Creates a hash set with a builder that an action fills, compared with the comparer. + /// + /// The action that adds the elements to the builder, which compares with the comparer. + /// The hash set of the elements that the action added. + /// + /// O(1) apart from the cost of the action. The member wraps + /// + /// and . + /// + [] + member build : action : (HashSet<'T>.Builder -> unit) -> HashSet<'T> + + /// + /// Creates an empty builder that compares with the comparer. + /// + /// The builder. + /// + /// O(1). The member wraps + /// . + /// + [] + member builder : unit -> HashSet<'T>.Builder + + // ---- Properties ---- + + /// + /// The equality comparer of the elements of the hash sets that the struct creates. + /// + /// + /// O(1). The default value of the struct returns . + /// + member Comparer : IEqualityComparer<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSet.fs b/src/FSharp.Collections.Immutable/HashSet/HashSet.fs new file mode 100644 index 00000000..f47229c6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSet.fs @@ -0,0 +1,339 @@ +[] +module FSharp.Collections.Immutable.HashSet + +open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Linq +// Opened after System.Collections.Generic, so that HashSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +// The functions call the members of ImmutableHashSet: Contains, TryGetValue, Add, Remove, Clear, the set algebra and the +// builders (ADR 0001, D5). The transformations that keep the element type collect their result in a builder with the +// comparer of the input; the ones that change it build a hash set with the default comparer of the new type (D5, +// Comparers). FSharp.Core sets cannot be null, so the null checks name the parameters of this module (D6). + +/// The struct with the default comparer: the creation functions are its members, so that each has one implementation (D12). +let private withDefaultComparer<'T> () = CustomComparerHashSet<'T>(EqualityComparer<'T>.Default) + +// ---- Creation ---- + +[] +[] +let empty<'T> : HashSet<'T> = ImmutableHashSet<'T>.Empty + +[] +let forComparer (comparer : IEqualityComparer<'T>) = CustomComparerHashSet<'T> comparer + +[] +let ofArray (array : 'T array) = (withDefaultComparer ()).ofArray array + +[] +let ofList (elements : 'T list) = (withDefaultComparer ()).ofList elements + +[] +let ofReadOnlySpan (span : ReadOnlySpan<'T>) = (withDefaultComparer ()).ofReadOnlySpan span + +[] +let ofSeq (elements : seq<'T>) = (withDefaultComparer ()).ofSeq elements + +[] +let ofSpan (span : Span<'T>) = (withDefaultComparer ()).ofSpan span + +[] +let singleton (value : 'T) = (withDefaultComparer ()).singleton value + +// ---- Builder ---- + +[] +let build (action : HashSet<'T>.Builder -> unit) = (withDefaultComparer ()).build action + +[] +let builder () : HashSet<'T>.Builder = (withDefaultComparer<'T>()).builder() + +[] +let ofBuilder (builder : HashSet<'T>.Builder) = + checkNotNull (nameof builder) builder + builder.ToImmutable () + +[] +let toBuilder (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.ToBuilder () + +[] +let update (action : HashSet<'T>.Builder -> unit) (set : HashSet<'T>) = + checkNotNull (nameof set) set + let builder = set.ToBuilder () + action builder + builder.ToImmutable () + +// ---- Properties ---- + +[] +let comparer (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.KeyComparer + +[] +let count (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.Count + +[] +let isEmpty (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.IsEmpty + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : HashSet<'T>) = + checkNotNull (nameof set) set + + match set.TryGetValue value with + | true, actual -> ValueSome actual + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let contains (element : 'T) (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.Contains element + +// exists and forall loop over the struct enumerator of the hash set, which allocates nothing. LINQ Any and All allocated +// 232 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 144 bytes, the boxed enumerator +// (measured in Release; a measured faster path replaces a higher priority, D5). Dispose returns the pooled stack of the +// enumerator. + +[] +let exists (predicate : 'T -> bool) (set : HashSet<'T>) = + checkNotNull (nameof set) set + let mutable enumerator = set.GetEnumerator () + + try + let mutable found = false + + while not found && enumerator.MoveNext () do + found <- predicate enumerator.Current + + found + finally + enumerator.Dispose () + +[] +let forall (predicate : 'T -> bool) (set : HashSet<'T>) = + checkNotNull (nameof set) set + let mutable enumerator = set.GetEnumerator () + + try + let mutable satisfied = true + + while satisfied && enumerator.MoveNext () do + satisfied <- predicate enumerator.Current + + satisfied + finally + enumerator.Dispose () + +[] +let isProperSubset (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsProperSubsetOf set2 + +[] +let isProperSuperset (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + + // ImmutableHashSet.IsProperSupersetOf counts every element of the other sequence, so elements that the comparer of + // set1 finds equal count twice, against its documentation (ADR 0001, D14). The count is right when set2 compares with + // the same comparer; otherwise a proper superset is a superset that is not a subset, and only IsSubsetOf allocates, + // one helper set of set2. + if obj.ReferenceEquals (set1.KeyComparer, set2.KeyComparer) then + set1.IsProperSupersetOf set2 + else + set1.IsSupersetOf set2 && not (set1.IsSubsetOf set2) + +[] +let isSubset (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSubsetOf set2 + +[] +let isSuperset (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSupersetOf set2 + +[] +let overlaps (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Overlaps set2 + +[] +let setEquals (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SetEquals set2 + +// ---- Transformation ---- + +[] +let add (value : 'T) (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.Add value + +[] +let addRange (elements : seq<'T>) (set : HashSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + let result = set.Union elements + + // Union returns the input itself when it adds nothing, except for an empty input and a hash set as the argument: it + // then returns a hash set built from the argument, another instance even when the argument is empty (ADR 0001, + // D14). Union only adds, so an unchanged count means that nothing was added. + if result.Count = set.Count then set else result + +[] +let choose (chooser : 'T -> 'U voption) (set : HashSet<'T>) = + checkNotNull (nameof set) set + HashSets.choose chooser set + +[] +let clear (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.Clear () + +[] +let difference (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Except set2 + +[] +let filter (predicate : 'T -> bool) (set : HashSet<'T>) = + checkNotNull (nameof set) set + let builder = ImmutableHashSet.CreateBuilder<'T> set.KeyComparer + + for item in set do + if predicate item then + builder.Add item |> ignore + + builder.ToImmutable () + +[] +let intersect (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Intersect set2 + +[] +let intersectMany (sets : seq>) = + // Set.intersectMany is Seq.reduce over Set.intersect, so its checks are those of Seq.reduce + checkNotNullOrDefaultSeq (nameof sets) sets + + // The type of a set is annotated, because the sequence that fixes it comes last + let checkSet (set : HashSet<'T>) = checkNotNull (nameof sets) set + + match Sets.reduce checkSet (fun set1 set2 -> set1.Intersect set2) sets with + | ValueSome result -> result + | ValueNone -> invalidArg (nameof sets) ErrorStrings.InputSequenceEmpty + +[] +let map (mapping : 'T -> 'U) (set : HashSet<'T>) = + checkNotNull (nameof set) set + HashSets.map mapping set + +[] +let remove (value : 'T) (set : HashSet<'T>) = + checkNotNull (nameof set) set + set.Remove value + +[] +let removeRange (elements : seq<'T>) (set : HashSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + set.Except elements + +[] +let symmetricDifference (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SymmetricExcept set2 + +[] +let union (set1 : HashSet<'T>) (set2 : HashSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Union set2 + +[] +let unionMany (sets : seq>) = + checkNotNullOrDefaultSeq (nameof sets) sets + + let checkSet (set : HashSet<'T>) = checkNotNull (nameof sets) set + + // Set.unionMany folds from the empty set; starting from the first set instead keeps its comparer + match Sets.reduce checkSet (fun set1 set2 -> set1.Union set2) sets with + | ValueSome result -> result + | ValueNone -> empty + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'T -> 'State) (state : 'State) (set : HashSet<'T>) = + checkNotNull (nameof set) set + Seq.fold folder state set + +[] +let foldBack (folder : 'T -> 'State -> 'State) (set : HashSet<'T>) (state : 'State) = + checkNotNull (nameof set) set + Seq.foldBack folder set state + +[] +let iter (action : 'T -> unit) (set : HashSet<'T>) = + checkNotNull (nameof set) set + Seq.iter action set + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : HashSet<'T>) = + checkNotNull (nameof set) set + let accepted = ImmutableHashSet.CreateBuilder<'T> set.KeyComparer + let rejected = ImmutableHashSet.CreateBuilder<'T> set.KeyComparer + + for item in set do + if predicate item then + accepted.Add item |> ignore + else + rejected.Add item |> ignore + + struct (accepted.ToImmutable (), rejected.ToImmutable ()) + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : HashSet<'T>) = + checkNotNull (nameof set) set + HashSets.partitionWith partitioner set + +// ---- Conversion ---- + +[] +let toArray (set : HashSet<'T>) = + checkNotNull (nameof set) set + Enumerable.ToArray set + +[] +let toList (set : HashSet<'T>) = + checkNotNull (nameof set) set + List.ofSeq set + +[] +let toSeq (set : HashSet<'T>) = + checkNotNull (nameof set) set + set :> seq<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSet.fsi b/src/FSharp.Collections.Immutable/HashSet/HashSet.fsi new file mode 100644 index 00000000..d7a6ad65 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSet.fsi @@ -0,0 +1,731 @@ +/// +/// Functions for , the immutable hash set. +/// +/// +/// +/// The module mirrors : each function has the semantics, the +/// argument order 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. Every function throws for a hash set. +/// +/// +/// The order in which a hash set enumerates its elements is unspecified. The functions that visit the elements visit them +/// in that order. +/// +/// +/// The creation functions create hash sets that compare with +/// . +/// +/// creates a , whose members create hash sets with +/// another comparer (ADR 0001, D12). +/// +/// +/// The transformations that keep the element type keep the comparer of their input; the set algebra keeps the comparer of +/// the first set. The transformations that change the element type compare the new elements with the default comparer of +/// their type. +/// +/// +/// 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.HashSet + +open System +open System.Collections.Generic +open System.Collections.Immutable +// Opened after System.Collections.Generic, so that HashSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +// ---- Creation ---- + +/// +/// The empty hash set, which compares with . +/// +/// +/// O(1). The value is . +/// +[] +[] +val empty<'T> : HashSet<'T> + +/// +/// Creates the struct that creates hash sets that compare their elements with an equality comparer. +/// +/// The equality comparer of the elements. +/// +/// The whose members are the creation functions of +/// this module with the comparer: they create hash sets and builders that compare with it. +/// +/// Thrown when is . +/// +/// +/// O(1). Creating the struct allocates nothing. +/// +/// also gives an existing hash set the comparer (ADR 0001, D12). +/// +/// +/// A comparer throws, where the BCL members Create, CreateRange, +/// CreateBuilder and WithComparer take it as the default comparer. For that meaning use +/// , which gives +/// a hash set the default comparer, or the default value of the struct, which compares with +/// . +/// +/// +/// +/// +/// let names = (HashSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "Ann"; "ANN"; "Bob" ] +/// +/// Evaluates to a hash set of "Ann" and "Bob" that compares without case. +/// +[] +val forComparer : comparer : IEqualityComparer<'T> -> CustomComparerHashSet<'T> + +/// +/// Creates a hash set from the elements of an array. +/// +/// The input array. +/// The hash set of the distinct elements of the array. +/// Thrown when is . +/// +/// O(n log n). Later changes to the array do not change the hash set. +/// +[] +val ofArray : array : 'T array -> HashSet<'T> + +/// Creates a hash set from the elements of a list. +/// The input list. +/// The hash set of the distinct elements of the list. +/// Thrown when is . +/// O(n log n). +[] +val ofList : elements : 'T list -> HashSet<'T> + +/// +/// Creates a hash set from the elements of a read-only span. +/// +/// The input span. +/// +/// The hash set of the distinct elements of the span; an empty span gives the empty hash set. +/// +/// +/// O(n log n). Later changes to the memory under the span do not change the hash set. The function wraps +/// . +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan<'T> -> HashSet<'T> + +/// +/// Creates a hash set from the elements of a sequence. +/// +/// The input sequence. +/// The hash set of the distinct elements of the sequence. +/// Thrown when is or a default flat list. +/// +/// O(n log n), or O(1) for a hash set that compares with the default comparer: the function returns such a hash set +/// without a copy, and gives a hash set with another comparer the default one. The function wraps +/// . +/// +[] +val ofSeq : elements : seq<'T> -> HashSet<'T> + +/// +/// Creates a hash set from the elements of a span. +/// +/// The input span. +/// +/// The hash set of the distinct elements of the span; an empty span gives the empty hash set. +/// +/// +/// O(n log n). Later changes to the memory under the span do not change the hash set. The function converts the span to a +/// read-only span and wraps +/// . +/// +[] +val ofSpan : span : Span<'T> -> HashSet<'T> + +/// Creates a hash set of one element. +/// The element. +/// The hash set of one element. +/// O(1). +[] +val singleton : value : 'T -> HashSet<'T> + +// ---- Builder ---- + +/// +/// Creates a hash set with a builder that an action fills. +/// +/// +/// The action that adds the elements to the builder, which compares with +/// . +/// +/// The hash set of the elements that the action added. +/// +/// O(1) apart from the cost of the action. The function wraps +/// +/// and . +/// +[] +val build : action : (HashSet<'T>.Builder -> unit) -> HashSet<'T> + +/// +/// Creates an empty builder that compares with . +/// +/// The builder. +/// +/// O(1). The function wraps +/// . +/// +[] +val builder : unit -> HashSet<'T>.Builder + +/// +/// Returns the contents of a builder as a hash set. +/// +/// The builder, which stays usable. +/// The hash set of the elements of the builder, with the comparer of the builder. +/// Thrown when is . +/// +/// O(1) apart from freezing the nodes that changed since the builder last returned a hash set: the hash set shares the +/// nodes of the builder instead of copying the elements. The function wraps +/// . +/// +[] +val ofBuilder : builder : HashSet<'T>.Builder -> HashSet<'T> + +/// +/// Creates a builder that contains the elements of a hash set. +/// +/// The input hash set, which stays unchanged. +/// The builder, with the comparer of the hash set. +/// Thrown when is . +/// +/// O(1). The function wraps . The builder shares +/// the nodes of the hash set until it changes them. +/// +[] +val toBuilder : set : HashSet<'T> -> HashSet<'T>.Builder + +/// +/// Creates a hash set from a builder over the elements of a hash set, after an action changes the builder. +/// +/// The action that changes the builder, which has the comparer of the hash set. +/// The input hash set, which stays unchanged. +/// The hash set of the elements of the changed builder, with the comparer of the input. +/// Thrown when is . +/// +/// O(1) apart from the cost of the action. The function wraps +/// and +/// . +/// +[] +val update : action : (HashSet<'T>.Builder -> unit) -> set : HashSet<'T> -> HashSet<'T> + +// ---- Properties ---- + +/// +/// Returns the equality comparer of the elements of the hash set. +/// +/// The input hash set. +/// The equality comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val comparer : set : HashSet<'T> -> IEqualityComparer<'T> + +/// Returns the number of elements of the hash set. +/// The input hash set. +/// The number of elements. +/// Thrown when is . +/// O(1). +[] +val count : set : HashSet<'T> -> int + +/// Returns whether the hash set has no elements. +/// The input hash set. +/// when the hash set is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : set : HashSet<'T> -> bool + +// ---- Access ---- + +/// +/// Returns the element of the hash set that equals a value, or ValueNone when there is none. +/// +/// The value to search for. +/// The input hash set. +/// The element that the comparer of the hash set considers equal to the value, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function wraps . The +/// element can differ from an equal value, for example in case under a case-insensitive comparer; the function returns +/// the instance that the hash set holds. +/// +[] +val tryGetValue : value : 'T -> set : HashSet<'T> -> 'T voption + +// ---- Search ---- + +/// +/// Returns whether the hash set contains an element equal to a value, in the comparer of the hash set. +/// +/// The value to find. +/// The input hash set. +/// when an element equals the value. +/// Thrown when is . +/// +/// O(log n). +/// +[] +val contains : element : 'T -> set : HashSet<'T> -> bool + +/// +/// Returns whether any element satisfies the predicate. +/// +/// The function to test the elements. +/// The input hash set. +/// when an element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that satisfies the predicate. +/// +[] +val exists : predicate : ('T -> bool) -> set : HashSet<'T> -> bool + +/// +/// Returns whether all elements satisfy the predicate. +/// +/// The function to test the elements. +/// The input hash set. +/// when every element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that does not satisfy the predicate. +/// +[] +val forall : predicate : ('T -> bool) -> set : HashSet<'T> -> bool + +/// +/// Returns whether every element of the first hash set is in the second one, and the second one has an element that the +/// first one does not have. +/// +/// The potential subset. +/// The set to test against. +/// when the first hash set is a proper subset of the second one. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val isProperSubset : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +/// +/// Returns whether every element of the second hash set is in the first one, and the first one has an element that the +/// second one does not have. +/// +/// The potential superset. +/// The set to test against. +/// when the first hash set is a proper superset of the second one. +/// Thrown when or is . +/// +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set, and O(m) more for a helper +/// set when the hash sets compare with different comparers. +/// +/// +/// The function compares in the comparer of the first hash set, with set semantics: elements of the second hash set that +/// this comparer finds equal count once, as the documentation of +/// +/// says, while that member counts each of them (ADR 0001, D14). It wraps the member when both hash sets have the same +/// comparer, and otherwise answers with +/// +/// and . +/// +/// +[] +val isProperSuperset : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +/// +/// Returns whether every element of the first hash set is in the second one. +/// +/// The potential subset. +/// The set to test against. +/// when the first hash set is a subset of the second one. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val isSubset : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +/// +/// Returns whether every element of the second hash set is in the first one. +/// +/// The potential superset. +/// The set to test against. +/// when the first hash set is a superset of the second one. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val isSuperset : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +/// +/// Returns whether the hash sets have an element in common. +/// +/// The first hash set. +/// The second hash set. +/// when an element of the second hash set is in the first one. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val overlaps : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +/// +/// Returns whether the hash sets have the same elements. +/// +/// The first hash set. +/// The second hash set. +/// when every element of each hash set is in the other one. +/// Thrown when or is . +/// +/// O((n + m) log (n + m)), where n and m are the numbers of elements of the first and the second hash set. The function +/// wraps +/// and compares in the comparer of the first hash set. +/// +[] +val setEquals : set1 : HashSet<'T> -> set2 : HashSet<'T> -> bool + +// ---- Transformation ---- + +/// Returns a hash set with a value added. +/// The value to add. +/// The input hash set. +/// +/// A hash set of the elements and the value, with the comparer of the input; the input itself when it already contains +/// an element equal to the value. +/// +/// Thrown when is . +/// O(log n). +[] +val add : value : 'T -> set : HashSet<'T> -> HashSet<'T> + +/// +/// Returns a hash set with the elements of a sequence added. +/// +/// The elements to add. +/// The input hash set. +/// +/// A hash set of the elements and the elements of the sequence, with the comparer of the input; the input itself when the +/// sequence adds no element. +/// +/// Thrown when is or a default flat list, or is . +/// +/// +/// O(m log (n + m)), where n and m are the numbers of elements of the hash set and the sequence. The function wraps +/// . +/// +/// +/// An element of the sequence that the comparer of the hash set considers equal to an element of the hash set does not +/// replace it, and of several equal elements of the sequence the first one is added (ADR 0001, D14). +/// +/// +[] +val addRange : elements : seq<'T> -> set : HashSet<'T> -> HashSet<'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 hash set. +/// +/// The hash set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n log n). The comparer of the input is not kept, even when the chosen values have the type of the elements: the +/// function cannot keep it for every type without a type test that would make its result depend on the type. +/// +[] +val choose : chooser : ('T -> 'U voption) -> set : HashSet<'T> -> HashSet<'U> + +/// +/// Returns an empty hash set with the comparer of the input. +/// +/// The input hash set. +/// The empty hash set; the input itself when it is empty. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val clear : set : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements of the first hash set that are not in the second one. +/// +/// The first hash set. +/// The hash set whose elements are removed from the first one. +/// The difference, with the comparer of the first hash set. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val difference : set1 : HashSet<'T> -> set2 : HashSet<'T> -> HashSet<'T> + +/// Returns the elements that satisfy the predicate. +/// The function to test the elements. +/// The input hash set. +/// The hash set of the elements that satisfy the predicate, with the comparer of the input. +/// Thrown when is . +/// O(n log n). +[] +val filter : predicate : ('T -> bool) -> set : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements that are in both hash sets. +/// +/// The first hash set. +/// The second hash set. +/// The intersection, with the comparer of the first hash set. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// , +/// which compares in the comparer of the first hash set and keeps the elements of the second hash set: where the +/// comparer considers two different elements equal, the intersection holds the one of the second hash set. +/// +[] +val intersect : set1 : HashSet<'T> -> set2 : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements that are in every hash set of a sequence. +/// +/// The hash sets. +/// The intersection, with the comparer of the first hash set. +/// Thrown when is or a default flat list, or one of its hash sets is . +/// Thrown when the sequence is empty. +/// +/// O(N log n), where N is the total number of elements of the other hash sets and n the number of elements of the first +/// one. The function intersects the hash sets from the first one to the last one. +/// +[] +val intersectMany : sets : seq> -> HashSet<'T> + +/// +/// Returns the results of a function applied to each element. +/// +/// The function to transform the elements. +/// The input hash set. +/// +/// The hash set of the distinct results, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n log n). The comparer of the input is not kept, even when the results have the type of the elements: the function +/// cannot keep it for every type without a type test that would make its result depend on the type. +/// +[] +val map : mapping : ('T -> 'U) -> set : HashSet<'T> -> HashSet<'U> + +/// Returns a hash set without an element equal to a value. +/// The value to remove. +/// The input hash set. +/// +/// A hash set of the other elements, with the comparer of the input; the input itself when it has no element equal to +/// the value. +/// +/// Thrown when is . +/// O(log n). +[] +val remove : value : 'T -> set : HashSet<'T> -> HashSet<'T> + +/// +/// Returns a hash set without the elements that are equal to an element of a sequence. +/// +/// The elements to remove; an element without an equal in the hash set is skipped. +/// The input hash set. +/// +/// A hash set of the other elements, with the comparer of the input; the input itself when it has no element equal to an +/// element of the sequence. +/// +/// Thrown when is or a default flat list, or is . +/// +/// O(m log n), where n and m are the numbers of elements of the hash set and the sequence. The function wraps +/// +/// and compares in the comparer of the hash set. +/// +[] +val removeRange : elements : seq<'T> -> set : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements that are in exactly one of the hash sets. +/// +/// The first hash set. +/// The second hash set. +/// The symmetric difference, with the comparer of the first hash set. +/// Thrown when or is . +/// +/// O((n + m) log (n + m)), where n and m are the numbers of elements of the first and the second hash set. The function +/// wraps +/// and compares in the comparer of the first hash set. +/// +[] +val symmetricDifference : set1 : HashSet<'T> -> set2 : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements that are in either hash set. +/// +/// The first hash set. +/// The second hash set. +/// The union, with the comparer of the first hash set. +/// Thrown when or is . +/// +/// O(m log (n + m)), where n and m are the numbers of elements of the first and the second hash set. The function wraps +/// +/// and compares in the comparer of the first hash set. +/// +[] +val union : set1 : HashSet<'T> -> set2 : HashSet<'T> -> HashSet<'T> + +/// +/// Returns the elements that are in any hash set of a sequence. +/// +/// The hash sets. +/// +/// The union, with the comparer of the first hash set; the empty hash set with the default comparer for an empty +/// sequence. +/// +/// Thrown when is or a default flat list, or one of its hash sets is . +/// +/// O(N log N), where N is the total number of elements of the hash sets. The function unites the hash sets from the first +/// one to the last one. +/// +[] +val unionMany : sets : seq> -> HashSet<'T> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each element, in the order of enumeration. +/// +/// The function that computes the next state. +/// The initial state. +/// The input hash set. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'T -> 'State) -> state : 'State -> set : HashSet<'T> -> 'State + +/// +/// Applies a function to each element and a state, in the reverse order of enumeration. +/// +/// The function that computes the next state. +/// The input hash set. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function copies the elements into an array first. +/// +[] +val foldBack : folder : ('T -> 'State -> 'State) -> set : HashSet<'T> -> state : 'State -> 'State + +/// +/// Applies an action to each element, in the order of enumeration. +/// +/// The action. +/// The input hash set. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('T -> unit) -> set : HashSet<'T> -> unit + +// ---- Partitioning ---- + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input hash set. +/// +/// The hash set of the elements that satisfy the predicate and the hash set of the other ones, both with the comparer of +/// the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : predicate : ('T -> bool) -> set : HashSet<'T> -> struct (HashSet<'T> * HashSet<'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 hash set. +/// +/// The hash set of the values of the first choices and the hash set of the values of the second choices, which compare +/// with . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : HashSet<'T> -> struct (HashSet<'T1> * HashSet<'T2>) + +// ---- Conversion ---- + +/// +/// Returns a new array with the elements, in the order of enumeration. +/// +/// The input hash set. +/// A copy of the elements. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : set : HashSet<'T> -> 'T array + +/// Returns a list of the elements, in the order of enumeration. +/// The input hash set. +/// The list of the elements. +/// Thrown when is . +/// O(n). +[] +val toList : set : HashSet<'T> -> 'T list + +/// Returns the hash set as a sequence. +/// The input hash set. +/// The sequence of the elements. +/// Thrown when is . +/// O(1). +[] +val toSeq : set : HashSet<'T> -> seq<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs b/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs new file mode 100644 index 00000000..b4f21d4c --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fs @@ -0,0 +1,85 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Immutable + +// System.Collections.Generic is not opened: its HashSet would shadow the HashSet abbreviation of this namespace. + +type HashSetBuffer<'T> = HashSet<'T>.Builder -> unit + +[] +type HashSetCE<'T> (comparer : System.Collections.Generic.IEqualityComparer<'T>) = + + // The builder checks its comparer when it is created, as CustomComparerHashSet does, not when an expression runs + do checkNotNull (nameof comparer) comparer + + new () = HashSetCE<'T>(System.Collections.Generic.EqualityComparer<'T>.Default) + + member inline _.Zero () : HashSetBuffer<'T> = ignore + + member inline _.Yield (item : 'T) : HashSetBuffer<'T> = fun builder -> builder.Add item |> ignore + + // Each input is checked before the buffer is returned, so that the exception names the parameter of the expression and + // not the parameter of UnionWith (other); a default flat list given as a sequence is rejected as FlatListCE rejects it + // (ADR 0001, D6) + member inline _.YieldFrom (items : seq<'T>) : HashSetBuffer<'T> = + checkNotNullOrDefaultSeq (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (items : 'T array) : HashSetBuffer<'T> = + checkNotNull (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (items : HashSet<'T>) : HashSetBuffer<'T> = + checkNotNull (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (buffer : HashSetBuffer<'T>) : HashSetBuffer<'T> = buffer + + member inline _.Combine (first : HashSetBuffer<'T>, second : HashSetBuffer<'T>) : HashSetBuffer<'T> = + fun builder -> + first builder + second builder + + member inline _.Delay ([] delayed : unit -> HashSetBuffer<'T>) : HashSetBuffer<'T> = + fun builder -> delayed () builder + + member inline _.For (source : seq<'U>, [] body : 'U -> HashSetBuffer<'T>) : HashSetBuffer<'T> = + // A null source would throw NullReferenceException and a default flat list InvalidOperationException when the loop + // enumerates; the expression rejects both as yield! does + checkNotNullOrDefaultSeq (nameof source) source + + fun builder -> + for item in source do + body item builder + + member inline _.While ([] guard : unit -> bool, body : HashSetBuffer<'T>) : HashSetBuffer<'T> = + fun builder -> + while guard () do + body builder + + member inline _.TryWith + (body : HashSetBuffer<'T>, [] handler : exn -> HashSetBuffer<'T>) + : HashSetBuffer<'T> + = + fun builder -> + try + body builder + with ex -> + handler ex builder + + member inline _.TryFinally (body : HashSetBuffer<'T>, [] compensation : unit -> unit) : HashSetBuffer<'T> = + fun builder -> + try + body builder + finally + compensation () + + member inline _.Using (resource : 'Resource, [] body : 'Resource -> HashSetBuffer<'T>) : HashSetBuffer<'T> = + fun builder -> + use resource = resource + body resource builder + + member _.Run (buffer : HashSetBuffer<'T>) : HashSet<'T> = + let builder = ImmutableHashSet.CreateBuilder<'T> comparer + buffer builder + builder.ToImmutable () diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fsi b/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fsi new file mode 100644 index 00000000..e3008cd6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetCE.fsi @@ -0,0 +1,85 @@ +namespace FSharp.Collections.Immutable + +open System + +// System.Collections.Generic is not opened: its HashSet would shadow the HashSet abbreviation of this namespace. + +/// +/// The body of a hashSet { } expression: a function that adds the elements to a builder. +/// +type HashSetBuffer<'T> = HashSet<'T>.Builder -> unit + +/// +/// The builder of the hashSet { } computation expression. +/// +/// +/// The expression collects its elements in an with +/// the equality comparer of the builder (ADR 0001, D7). An element equal to an earlier one is not added again. +/// hashSet { } compares with ; an instance created +/// with a comparer builds hash sets that compare with it. +/// +[] +type HashSetCE<'T> = + + /// + /// Creates the builder of hash sets that compare with an equality comparer. + /// + /// The equality comparer of the elements of the hash set. + /// Thrown when is . + new : comparer : System.Collections.Generic.IEqualityComparer<'T> -> HashSetCE<'T> + + /// + /// Creates the builder of hash sets that compare with . + /// + new : unit -> HashSetCE<'T> + + /// An expression without elements. + member inline Zero : unit -> HashSetBuffer<'T> + + /// Adds an element. + member inline Yield : item : 'T -> HashSetBuffer<'T> + + /// Adds the elements of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline YieldFrom : items : seq<'T> -> HashSetBuffer<'T> + + /// Adds the elements of an array. + /// Thrown when the array is . + member inline YieldFrom : items : 'T array -> HashSetBuffer<'T> + + /// Adds the elements of a hash set. + /// Thrown when the hash set is . + member inline YieldFrom : items : HashSet<'T> -> HashSetBuffer<'T> + + /// Adds the elements of another expression body. + member inline YieldFrom : buffer : HashSetBuffer<'T> -> HashSetBuffer<'T> + + /// Adds the elements of two parts of the expression, in order. + member inline Combine : first : HashSetBuffer<'T> * second : HashSetBuffer<'T> -> HashSetBuffer<'T> + + /// + /// Runs the body of the expression each time the elements are collected. + /// + member inline Delay : [] delayed : (unit -> HashSetBuffer<'T>) -> HashSetBuffer<'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 -> HashSetBuffer<'T>) -> HashSetBuffer<'T> + + /// Adds the elements of the body while the guard holds. + member inline While : [] guard : (unit -> bool) * body : HashSetBuffer<'T> -> HashSetBuffer<'T> + + /// Handles the exceptions of the body. + member inline TryWith : + body : HashSetBuffer<'T> * [] handler : (exn -> HashSetBuffer<'T>) -> HashSetBuffer<'T> + + /// Runs the compensation after the body. + member inline TryFinally : body : HashSetBuffer<'T> * [] compensation : (unit -> unit) -> HashSetBuffer<'T> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> HashSetBuffer<'T>) -> HashSetBuffer<'T> + when 'Resource :> IDisposable | null + + /// Collects the elements of the expression. + member Run : buffer : HashSetBuffer<'T> -> HashSet<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fs b/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fs new file mode 100644 index 00000000..45ae6233 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fs @@ -0,0 +1,16 @@ +[] +module FSharp.Collections.Immutable.Option.HashSet + +open FSharp.Collections.Immutable + +// The functions delegate to the default module and convert between options and value options. + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : HashSet<'T>) = HashSet.tryGetValue value set |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U option) (set : HashSet<'T>) = HashSet.choose (fun item -> chooser item |> ValueOption.ofOption) set diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fsi b/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fsi new file mode 100644 index 00000000..130dfc7c --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetOption.fsi @@ -0,0 +1,49 @@ +/// +/// 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). has neither +/// function; they take and return options as FSharp.Core would. +/// +[] +module FSharp.Collections.Immutable.Option.HashSet + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the element of the hash set that equals a value, or None when there is none. +/// +/// The value to search for. +/// The input hash set. +/// The element that the comparer of the hash set considers equal to the value, or None. +/// Thrown when is . +/// +/// O(log n). The function wraps . The +/// element can differ from an equal value, for example in case under a case-insensitive comparer; the function returns +/// the instance that the hash set holds. +/// +[] +val tryGetValue : value : 'T -> set : HashSet<'T> -> 'T 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 hash set. +/// +/// The hash set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n log n). The comparer of the input is not kept, even when the chosen values have the type of the elements. +/// +[] +val choose : chooser : ('T -> 'U option) -> set : HashSet<'T> -> HashSet<'U> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fs b/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fs new file mode 100644 index 00000000..caaaad13 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fs @@ -0,0 +1,18 @@ +[] +module FSharp.Collections.Immutable.Tuple.HashSet + +open FSharp.Collections.Immutable + +// The functions delegate to the default module and repack its struct tuples. + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : HashSet<'T>) = + let struct (accepted, rejected) = HashSet.partition predicate set + accepted, rejected + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : HashSet<'T>) = + let struct (first, second) = HashSet.partitionWith partitioner set + first, second diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fsi b/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fsi new file mode 100644 index 00000000..ee400a2f --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetTuple.fsi @@ -0,0 +1,43 @@ +/// +/// The functions of that 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). Each function has the signature of the function with the same +/// name in . +/// +[] +module FSharp.Collections.Immutable.Tuple.HashSet + +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// Splits the elements by a predicate. +/// The function to test the elements. +/// The input hash set. +/// +/// The hash set of the elements that satisfy the predicate and the hash set of the other ones, both with the comparer of +/// the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : predicate : ('T -> bool) -> set : HashSet<'T> -> HashSet<'T> * HashSet<'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 hash set. +/// +/// The hash set of the values of the first choices and the hash set of the values of the second choices, which compare +/// with . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : HashSet<'T> -> HashSet<'T1> * HashSet<'T2> diff --git a/src/FSharp.Collections.Immutable/HashSet/HashSetTypes.fs b/src/FSharp.Collections.Immutable/HashSet/HashSetTypes.fs new file mode 100644 index 00000000..482f0e7d --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/HashSetTypes.fs @@ -0,0 +1,15 @@ +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 hash set, . +/// +/// +/// A hash set compares its elements with an equality comparer: , +/// unless a created it with another one. +/// +type HashSet<'T> = ImmutableHashSet<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/ISet.fs b/src/FSharp.Collections.Immutable/HashSet/ISet.fs new file mode 100644 index 00000000..4099213d --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISet.fs @@ -0,0 +1,252 @@ +[] +module FSharp.Collections.Immutable.ISet + +open System +open System.Collections.Immutable +open System.Linq + +// The functions call the members of IImmutableSet. A transformation that keeps the element type and builds a new set +// starts from Clear, which keeps the implementation and the comparer of its input, and fills it with Union (ADR 0001, +// D5). The interface cannot tell its comparer, so the transformations to another element type return a HashSet with the +// default comparer of that type. + +// ---- Properties ---- + +[] +let count (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Count + +[] +let isEmpty (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Count = 0 + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + + match set.TryGetValue value with + | true, actual -> ValueSome actual + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let contains (element : 'T) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Contains element + +// exists and forall call Seq, which allocates only the enumerator of the set: 144 bytes per call for a hash set and 64 for +// a sorted set, against 232 and 152 for LINQ Any and All, which also allocate a delegate (measured in Release; a measured +// faster path replaces a higher priority, D5). + +[] +let exists (predicate : 'T -> bool) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + Seq.exists predicate set + +[] +let forall (predicate : 'T -> bool) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + Seq.forall predicate set + +[] +let isProperSubset (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsProperSubsetOf set2 + +[] +let isProperSuperset (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + + // ImmutableHashSet and ImmutableSortedSet count every element of the other sequence in IsProperSupersetOf, so + // elements that the comparer of set1 finds equal count twice, against its documentation (ADR 0001, D14). A proper + // superset is a superset that is not a subset; the interface does not tell the comparers, and only IsSubsetOf + // allocates, one helper set of set2. + set1.IsSupersetOf set2 && not (set1.IsSubsetOf set2) + +[] +let isSubset (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSubsetOf set2 + +[] +let isSuperset (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSupersetOf set2 + +[] +let overlaps (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Overlaps set2 + +[] +let setEquals (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SetEquals set2 + +// ---- Transformation ---- + +[] +let add (value : 'T) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Add value + +[] +let addRange (elements : seq<'T>) (set : IImmutableSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + let result = set.Union elements + + // ImmutableHashSet returns another instance when an empty set receives a hash set as the argument, and the interface + // promises no instance at all. Union only adds, so an unchanged count means that nothing was added, and the input + // itself is the answer for every implementation (ADR 0001, D14). + if result.Count = set.Count then set else result + +[] +let choose (chooser : 'T -> 'U voption) (set : IImmutableSet<'T>) : HashSet<'U> = + checkNotNull (nameof set) set + HashSets.choose chooser set + +[] +let clear (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Clear () + +[] +let difference (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Except set2 + +[] +let filter (predicate : 'T -> bool) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + // LINQ Where with its delegate stays: Seq.filter allocates as many bytes and runs slower, a list of the accepted + // elements allocates more for large sets (32 536 against 28 336 bytes for a hash set of 1 000), and removing the + // rejected elements allocates ten times as much (measured in Release, D5) + set.Clear().Union(Enumerable.Where (set, Func<'T, bool> predicate)) + +[] +let intersect (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Intersect set2 + +[] +let intersectMany (sets : seq>) = + // Set.intersectMany is Seq.reduce over Set.intersect, so its checks are those of Seq.reduce + checkNotNullOrDefaultSeq (nameof sets) sets + + // The type of a set is annotated, because the sequence that fixes it comes last + let checkSet (set : IImmutableSet<'T>) = checkNotNull (nameof sets) set + + match Sets.reduce checkSet (fun set1 set2 -> set1.Intersect set2) sets with + | ValueSome result -> result + | ValueNone -> invalidArg (nameof sets) ErrorStrings.InputSequenceEmpty + +[] +let map (mapping : 'T -> 'U) (set : IImmutableSet<'T>) : HashSet<'U> = + checkNotNull (nameof set) set + HashSets.map mapping set + +[] +let remove (value : 'T) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set.Remove value + +[] +let removeRange (elements : seq<'T>) (set : IImmutableSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + // ImmutableHashSet and ImmutableSortedSet return the input itself when nothing is removed (ADR 0001, D14), so unlike + // in addRange no check of the result is needed + set.Except elements + +[] +let symmetricDifference (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SymmetricExcept set2 + +[] +let union (set1 : IImmutableSet<'T>) (set2 : IImmutableSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Union set2 + +[] +let unionMany (sets : seq>) = + checkNotNullOrDefaultSeq (nameof sets) sets + + let checkSet (set : IImmutableSet<'T>) = checkNotNull (nameof sets) set + + // Starting from the first set keeps its implementation and comparer; without a set, the result is a HashSet + match Sets.reduce checkSet (fun set1 set2 -> set1.Union set2) sets with + | ValueSome result -> result + | ValueNone -> ImmutableHashSet<'T>.Empty :> IImmutableSet<'T> + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'T -> 'State) (state : 'State) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + Seq.fold folder state set + +[] +let foldBack (folder : 'T -> 'State -> 'State) (set : IImmutableSet<'T>) (state : 'State) = + checkNotNull (nameof set) set + Seq.foldBack folder set state + +[] +let iter (action : 'T -> unit) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + Seq.iter action set + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + let accepted = ResizeArray<'T>() + let rejected = ResizeArray<'T>() + + for item in set do + if predicate item then + accepted.Add item + else + rejected.Add item + + let empty = set.Clear () + struct (empty.Union accepted, empty.Union rejected) + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : IImmutableSet<'T>) : struct (HashSet<'T1> * HashSet<'T2>) = + checkNotNull (nameof set) set + HashSets.partitionWith partitioner set + +// ---- Conversion ---- + +[] +let toArray (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + Enumerable.ToArray set + +[] +let toList (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + List.ofSeq set + +[] +let toSeq (set : IImmutableSet<'T>) = + checkNotNull (nameof set) set + set :> seq<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/ISet.fsi b/src/FSharp.Collections.Immutable/HashSet/ISet.fsi new file mode 100644 index 00000000..e70316b6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISet.fsi @@ -0,0 +1,582 @@ +/// +/// Functions for , the interface of the immutable sets. +/// +/// +/// +/// The module mirrors as +/// does: each function has the semantics, the argument order and the exceptions of the function with the same name in +/// that module (ADR 0001). Functions that FSharp.Core lacks wrap a member of the interface. Every function throws +/// for a set. +/// +/// +/// The module has no type abbreviation, because an abbreviation named after it would shadow +/// . +/// +/// +/// The transformations that keep the element type return the implementation of their input with its comparer: they call +/// the members of the interface, and the ones that build a new set start from +/// . The set algebra keeps the implementation and the +/// comparer of the first set. The interface cannot tell its comparer, so the transformations that change the element +/// type return a that compares with +/// . +/// +/// +/// The interface can be implemented by a hash set, a sorted set or another type, so the complexity of most functions +/// depends on the implementation. Their remarks give it for , +/// and also for where it is the same. The functions that +/// visit the elements visit each of them once, in the order of enumeration of the implementation. +/// +/// +[] +module FSharp.Collections.Immutable.ISet + +open System.Collections.Immutable + +// ---- Properties ---- + +/// +/// Returns the number of elements of the set. +/// +/// The input set. +/// The number of elements. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(1) for +/// and . +/// +[] +val count : set : IImmutableSet<'T> -> int + +/// +/// Returns whether the set has no elements. +/// +/// The input set. +/// when the set is empty. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(1) for +/// and . +/// +[] +val isEmpty : set : IImmutableSet<'T> -> bool + +// ---- Access ---- + +/// +/// Returns the element of the set that equals a value, or ValueNone when there is none. +/// +/// The value to search for. +/// The input set. +/// The element that the comparer of the set considers equal to the value, or ValueNone. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(log n) for +/// and . +/// The function wraps . The element can +/// differ from an equal value, for example in case under a case-insensitive comparer; the function returns the instance +/// that the set holds. +/// +[] +val tryGetValue : value : 'T -> set : IImmutableSet<'T> -> 'T voption + +// ---- Search ---- + +/// +/// Returns whether the set contains an element equal to a value, in the comparer of the set. +/// +/// The value to find. +/// The input set. +/// when an element equals the value. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(log n) for +/// and . +/// +[] +val contains : element : 'T -> set : IImmutableSet<'T> -> bool + +/// +/// Returns whether any element satisfies the predicate. +/// +/// The function to test the elements. +/// The input set. +/// when an element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that satisfies the predicate. +/// +[] +val exists : predicate : ('T -> bool) -> set : IImmutableSet<'T> -> bool + +/// +/// Returns whether all elements satisfy the predicate. +/// +/// The function to test the elements. +/// The input set. +/// when every element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function stops at the first element that does not satisfy the predicate. +/// +[] +val forall : predicate : ('T -> bool) -> set : IImmutableSet<'T> -> bool + +/// +/// Returns whether every element of the first set is in the second one, and the second one has an element that the first +/// one does not have. +/// +/// The potential subset. +/// The set to test against. +/// when the first set is a proper subset of the second one. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// +/// and compares in the comparer of the first set. +/// +[] +val isProperSubset : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +/// +/// Returns whether every element of the second set is in the first one, and the first one has an element that the second +/// one does not have. +/// +/// The potential superset. +/// The set to test against. +/// when the first set is a proper superset of the second one. +/// Thrown when or is . +/// +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set, with a helper set of the second set. +/// +/// +/// The function compares in the comparer of the first set, with set semantics: elements of the second set that this +/// comparer finds equal count once, as the documentation of +/// +/// says, while and +/// count each of them (ADR 0001, D14). It answers with +/// +/// and . +/// +/// +[] +val isProperSuperset : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +/// +/// Returns whether every element of the first set is in the second one. +/// +/// The potential subset. +/// The set to test against. +/// when the first set is a subset of the second one. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// +/// and compares in the comparer of the first set. +/// +[] +val isSubset : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +/// +/// Returns whether every element of the second set is in the first one. +/// +/// The potential superset. +/// The set to test against. +/// when the first set is a superset of the second one. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// +/// and compares in the comparer of the first set. +/// +[] +val isSuperset : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +/// +/// Returns whether the sets have an element in common. +/// +/// The first set. +/// The second set. +/// when an element of the second set is in the first one. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// +/// and compares in the comparer of the first set. +/// +[] +val overlaps : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +/// +/// Returns whether the sets have the same elements. +/// +/// The first set. +/// The second set. +/// when every element of each set is in the other one. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O((n + m) log (n + m)) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// +/// and compares in the comparer of the first set. +/// +[] +val setEquals : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> bool + +// ---- Transformation ---- + +/// +/// Returns a set with a value added. +/// +/// The value to add. +/// The input set. +/// A set of the elements and the value, with the implementation and the comparer of the input. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(log n) for +/// and . +/// The function wraps . +/// +[] +val add : value : 'T -> set : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns a set with the elements of a sequence added. +/// +/// The elements to add. +/// The input set. +/// +/// A set of the elements and the elements of the sequence, with the implementation and the comparer of the input; the +/// input itself when the sequence adds no element. +/// +/// Thrown when is or a default flat list, or is . +/// +/// +/// The complexity depends on the implementation of the set: O(m log (n + m)) for an +/// , where n and m are the numbers of elements of the set +/// and the sequence. The function wraps +/// . +/// +/// +/// Which of two elements that the comparer of the set considers equal stays depends on the implementation (ADR 0001, +/// D14). An keeps the element of the set, and the first +/// of several equal elements of the sequence. An keeps +/// the element of the set when the sequence is about as large as the set or smaller, and may replace it when the +/// sequence is much larger. +/// +/// +[] +val addRange : elements : seq<'T> -> set : IImmutableSet<'T> -> IImmutableSet<'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 set. +/// +/// The hash set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('T -> 'U voption) -> set : IImmutableSet<'T> -> HashSet<'U> + +/// +/// Returns an empty set with the implementation and the comparer of the input. +/// +/// The input set. +/// The empty set. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(1) for +/// and . +/// The function wraps . +/// +[] +val clear : set : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements of the first set that are not in the second one. +/// +/// The first set. +/// The set whose elements are removed from the first one. +/// The difference, with the implementation and the comparer of the first set. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// . +/// +[] +val difference : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that satisfy the predicate. +/// +/// The function to test the elements. +/// The input set. +/// The set of the elements that satisfy the predicate, with the implementation and the comparer of the input. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(n log n) for +/// and . +/// +[] +val filter : predicate : ('T -> bool) -> set : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that are in both sets. +/// +/// The first set. +/// The second set. +/// The intersection, with the implementation and the comparer of the first set. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log n) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// . +/// +[] +val intersect : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that are in every set of a sequence. +/// +/// The sets. +/// The intersection, with the implementation and the comparer of the first set. +/// Thrown when is or a default flat list, or one of its sets is . +/// Thrown when the sequence is empty. +/// +/// The complexity depends on the implementation of the first set: O(N log n) for an +/// , where N is the total number of elements of the other +/// sets and n the number of elements of the first one. The function intersects the sets from the first one to the last one. +/// +[] +val intersectMany : sets : seq> -> IImmutableSet<'T> + +/// Returns the results of a function applied to each element. +/// The function to transform the elements. +/// The input set. +/// +/// The hash set of the distinct results, which compares with +/// . +/// +/// Thrown when is . +/// O(n log n). +[] +val map : mapping : ('T -> 'U) -> set : IImmutableSet<'T> -> HashSet<'U> + +/// +/// Returns a set without an element equal to a value. +/// +/// The value to remove. +/// The input set. +/// A set of the other elements, with the implementation and the comparer of the input. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(log n) for +/// and . +/// The function wraps . +/// +[] +val remove : value : 'T -> set : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns a set without the elements that are equal to an element of a sequence. +/// +/// The elements to remove; an element without an equal in the set is skipped. +/// The input set. +/// +/// A set of the other elements, with the implementation and the comparer of the input; the input itself when it has no +/// element equal to an element of the sequence. +/// +/// Thrown when is or a default flat list, or is . +/// +/// +/// The complexity depends on the implementation of the set: O(m log n) for an +/// , where n and m are the numbers of elements of the set +/// and the sequence. The function wraps +/// +/// and compares in the comparer of the set. +/// +/// +/// An and an +/// return the input itself from the wrapped member when +/// nothing is removed; the function does not check the result of another implementation. +/// +/// +[] +val removeRange : elements : seq<'T> -> set : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that are in exactly one of the sets. +/// +/// The first set. +/// The second set. +/// The symmetric difference, with the implementation and the comparer of the first set. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O((n + m) log (n + m)) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// . +/// +[] +val symmetricDifference : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that are in either set. +/// +/// The first set. +/// The second set. +/// The union, with the implementation and the comparer of the first set. +/// Thrown when or is . +/// +/// The complexity depends on the implementation of the first set: O(m log (n + m)) for an +/// , where n and m are the numbers of elements of the first +/// and the second set. The function wraps +/// . +/// +[] +val union : set1 : IImmutableSet<'T> -> set2 : IImmutableSet<'T> -> IImmutableSet<'T> + +/// +/// Returns the elements that are in any set of a sequence. +/// +/// The sets. +/// +/// The union, with the implementation and the comparer of the first set; for an empty sequence, the empty +/// , which compares with +/// . +/// +/// Thrown when is or a default flat list, or one of its sets is . +/// +/// The complexity depends on the implementation of the first set: O(N log N) for an +/// , where N is the total number of elements of the sets. +/// The function unites the sets from the first one to the last one. +/// +[] +val unionMany : sets : seq> -> IImmutableSet<'T> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each element, in the order of enumeration. +/// +/// The function that computes the next state. +/// The initial state. +/// The input set. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'T -> 'State) -> state : 'State -> set : IImmutableSet<'T> -> 'State + +/// +/// Applies a function to each element and a state, in the reverse order of enumeration. +/// +/// The function that computes the next state. +/// The input set. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function copies the elements into an array first. +/// +[] +val foldBack : folder : ('T -> 'State -> 'State) -> set : IImmutableSet<'T> -> state : 'State -> 'State + +/// +/// Applies an action to each element, in the order of enumeration. +/// +/// The action. +/// The input set. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('T -> unit) -> set : IImmutableSet<'T> -> unit + +// ---- Partitioning ---- + +/// +/// Splits the elements by a predicate. +/// +/// The function to test the elements. +/// The input set. +/// +/// The set of the elements that satisfy the predicate and the set of the other ones, both with the implementation and +/// the comparer of the input. +/// +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(n log n) for +/// and . +/// +[] +val partition : predicate : ('T -> bool) -> set : IImmutableSet<'T> -> struct (IImmutableSet<'T> * IImmutableSet<'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 set. +/// +/// The hash set of the values of the first choices and the hash set of the values of the second choices, which compare +/// with . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : IImmutableSet<'T> -> struct (HashSet<'T1> * HashSet<'T2>) + +// ---- Conversion ---- + +/// +/// Returns a new array with the elements, in the order of enumeration. +/// +/// The input set. +/// A copy of the elements. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : set : IImmutableSet<'T> -> 'T array + +/// Returns a list of the elements, in the order of enumeration. +/// The input set. +/// The list of the elements. +/// Thrown when is . +/// O(n). +[] +val toList : set : IImmutableSet<'T> -> 'T list + +/// Returns the set as a sequence. +/// The input set. +/// The sequence of the elements. +/// Thrown when is . +/// O(1). +[] +val toSeq : set : IImmutableSet<'T> -> seq<'T> diff --git a/src/FSharp.Collections.Immutable/HashSet/ISetOption.fs b/src/FSharp.Collections.Immutable/HashSet/ISetOption.fs new file mode 100644 index 00000000..52fb1f01 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISetOption.fs @@ -0,0 +1,18 @@ +[] +module FSharp.Collections.Immutable.Option.ISet + +open System.Collections.Immutable +open FSharp.Collections.Immutable + +// The functions delegate to the default module and convert between options and value options. + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : IImmutableSet<'T>) = ISet.tryGetValue value set |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U option) (set : IImmutableSet<'T>) = + ISet.choose (fun item -> chooser item |> ValueOption.ofOption) set diff --git a/src/FSharp.Collections.Immutable/HashSet/ISetOption.fsi b/src/FSharp.Collections.Immutable/HashSet/ISetOption.fsi new file mode 100644 index 00000000..97e1e5e5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISetOption.fsi @@ -0,0 +1,51 @@ +/// +/// 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). has neither +/// function; they take and return options as FSharp.Core would. +/// +[] +module FSharp.Collections.Immutable.Option.ISet + +open System.Collections.Immutable +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the element of the set that equals a value, or None when there is none. +/// +/// The value to search for. +/// The input set. +/// The element that the comparer of the set considers equal to the value, or None. +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(log n) for +/// and . +/// The function wraps . The element can +/// differ from an equal value, for example in case under a case-insensitive comparer; the function returns the instance +/// that the set holds. +/// +[] +val tryGetValue : value : 'T -> set : IImmutableSet<'T> -> 'T 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 set. +/// +/// The hash set of the distinct chosen values, which compares with +/// . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('T -> 'U option) -> set : IImmutableSet<'T> -> HashSet<'U> diff --git a/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fs b/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fs new file mode 100644 index 00000000..839c22b4 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fs @@ -0,0 +1,19 @@ +[] +module FSharp.Collections.Immutable.Tuple.ISet + +open System.Collections.Immutable +open FSharp.Collections.Immutable + +// The functions delegate to the default module and repack its struct tuples. + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : IImmutableSet<'T>) = + let struct (accepted, rejected) = ISet.partition predicate set + accepted, rejected + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : IImmutableSet<'T>) = + let struct (first, second) = ISet.partitionWith partitioner set + first, second diff --git a/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fsi b/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fsi new file mode 100644 index 00000000..5d739074 --- /dev/null +++ b/src/FSharp.Collections.Immutable/HashSet/ISetTuple.fsi @@ -0,0 +1,48 @@ +/// +/// The functions of that 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). +/// +[] +module FSharp.Collections.Immutable.Tuple.ISet + +open System.Collections.Immutable +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// +/// Splits the elements by a predicate. +/// +/// The function to test the elements. +/// The input set. +/// +/// The set of the elements that satisfy the predicate and the set of the other ones, both with the implementation and +/// the comparer of the input. +/// +/// Thrown when is . +/// +/// The complexity depends on the implementation: O(n log n) for +/// and . +/// +[] +val partition : predicate : ('T -> bool) -> set : IImmutableSet<'T> -> IImmutableSet<'T> * IImmutableSet<'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 set. +/// +/// The hash set of the values of the first choices and the hash set of the values of the second choices, which compare +/// with . +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val partitionWith : partitioner : ('T -> Choice<'T1, 'T2>) -> set : IImmutableSet<'T> -> HashSet<'T1> * HashSet<'T2> diff --git a/src/FSharp.Collections.Immutable/Helper/HashSets.fs b/src/FSharp.Collections.Immutable/Helper/HashSets.fs new file mode 100644 index 00000000..086271c0 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/HashSets.fs @@ -0,0 +1,45 @@ +/// +/// Algorithms that build hash sets with the default comparer of their element type (ADR 0001, D5, Comparers). +/// +/// +/// The transformations of the HashSet and ISet modules that change the element type share them: the +/// comparer of the input cannot compare elements of another type. +/// +module internal FSharp.Collections.Immutable.HashSets + +open System.Collections.Immutable + +/// The values that the chooser returns for the elements. +let inline choose ([] chooser : 'T -> 'U voption) (source : seq<'T>) : ImmutableHashSet<'U> = + let builder = ImmutableHashSet.CreateBuilder<'U>() + + for item in source do + match chooser item with + | ValueSome value -> builder.Add value |> ignore + | ValueNone -> () + + builder.ToImmutable () + +/// The results of the mapping of the elements. +let inline map ([] mapping : 'T -> 'U) (source : seq<'T>) : ImmutableHashSet<'U> = + let builder = ImmutableHashSet.CreateBuilder<'U>() + + for item in source do + builder.Add (mapping item) |> ignore + + builder.ToImmutable () + +/// The values of the first choices and the values of the second choices that the partitioner returns for the elements. +let inline partitionWith + ([] partitioner : 'T -> Choice<'T1, 'T2>) + (source : seq<'T>) + : struct (ImmutableHashSet<'T1> * ImmutableHashSet<'T2>) = + let first = ImmutableHashSet.CreateBuilder<'T1>() + let second = ImmutableHashSet.CreateBuilder<'T2>() + + for item in source do + match partitioner item with + | Choice1Of2 value -> first.Add value |> ignore + | Choice2Of2 value -> second.Add value |> ignore + + struct (first.ToImmutable (), second.ToImmutable ()) diff --git a/src/FSharp.Collections.Immutable/Helper/Sets.fs b/src/FSharp.Collections.Immutable/Helper/Sets.fs new file mode 100644 index 00000000..e8043608 --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/Sets.fs @@ -0,0 +1,33 @@ +/// +/// The algorithms that the set modules share, whatever the type of the set (ADR 0001, D5). +/// +module internal FSharp.Collections.Immutable.Sets + +/// +/// Combines the sets of a sequence from the first one to the last one, as Seq.reduce does, or returns +/// ValueNone for an empty sequence. +/// +/// +/// The caller checks the sequence and passes the check of each set, so that a set throws with the +/// name of the parameter of the caller. The result keeps the comparer and the implementation of the first set when +/// keeps those of its first argument, as the set algebra of the BCL does. +/// +let inline reduce + ([] checkSet : 'Set -> unit) + ([] combine : 'Set -> 'Set -> 'Set) + (sets : seq<'Set>) + : 'Set voption = + use enumerator = sets.GetEnumerator () + + if enumerator.MoveNext () then + let mutable result = enumerator.Current + checkSet result + + while enumerator.MoveNext () do + let next = enumerator.Current + checkSet next + result <- combine result next + + ValueSome result + else + ValueNone diff --git a/src/FSharp.Collections.Immutable/Sets.fs b/src/FSharp.Collections.Immutable/Sets.fs index 8d94e56e..563d94e7 100644 --- a/src/FSharp.Collections.Immutable/Sets.fs +++ b/src/FSharp.Collections.Immutable/Sets.fs @@ -1,140 +1,5 @@ namespace FSharp.Collections.Immutable -type ISet<'T> = System.Collections.Immutable.IImmutableSet<'T> - -type HashSet<'T> = System.Collections.Immutable.ImmutableHashSet<'T> -type HashSetBuilder<'T> = HashSet<'T>.Builder - -[] -module HashSet = - - type internal HashSetFactory = System.Collections.Immutable.ImmutableHashSet - - let inline check (set : HashSet<_>) = checkNotNull (nameof set) set - - ////////// Creating ////////// - - let inline empty<'T> = HashSetFactory.Create<'T>() - let inline singleton<'T> (item : 'T) = HashSetFactory.Create<'T>(item) - let inline ofSeq source = HashSetFactory.CreateRange (source) - let inline ofSeqWithComparer comparer source = HashSetFactory.Create (comparer, items = (source |> Array.ofSeq)) - let inline ofArray (source : _ array) = HashSetFactory.CreateRange (source) - - let inline ofBuilder (hashSetBuilder : HashSetBuilder<_>) = - checkNotNull (nameof hashSetBuilder) hashSetBuilder - hashSetBuilder.ToImmutable () - - let inline ofComparer<'T> comparer = HashSetFactory.Create<'T>(equalityComparer = comparer) - - let inline toSeq (set : HashSet<_>) = set :> seq<_> - - let inline toArray (set : HashSet<_>) = - check set - Seq.toArray set - - ////////// Building ////////// - - let inline builder () = HashSetFactory.CreateBuilder () - let inline builderWith capacity : HashSet<'T>.Builder = HashSetFactory.CreateBuilder (capacity) - let inline builderWithComparer comparer = HashSetFactory.CreateBuilder (comparer) - - let inline toBuilder set : HashSetBuilder<_> = - check set - set.ToBuilder () - - let inline keyComparer set = - check set - set.KeyComparer - - let inline length set = - check set - set.Count - - let inline isEmpty set = - check set - set.IsEmpty - - let inline contains value set = - check set - set.Contains value - - let inline exists predicate map = - check map - map |> Seq.exists predicate - - let inline isSubset (set1 : HashSet<_>) set2 = - check set1 - set1.IsSubsetOf set2 - - let inline isProperSubset (set1 : HashSet<_>) set2 = - check set1 - set1.IsProperSubsetOf set2 - - let inline isSuperset (set1 : HashSet<_>) set2 = - check set1 - set1.IsSupersetOf set2 - - let inline isProperSuperset (set1 : HashSet<_>) set2 = - check set1 - set1.IsProperSupersetOf set2 - - let inline add value set : HashSet<_> = - check set - set.Add (value) - - let inline union set values : HashSet<_> = - check set - values |> set.Union - - let inline unionMany (sets : HashSet<_> seq) = Seq.reduce union sets - - let inline intersect (set1 : HashSet<_>) set2 = - check set1 - set1.Intersect set2 - - let inline intersectMany (sets : HashSet<_> seq) = Seq.reduce intersect sets - - let inline remove value set : HashSet<_> = - check set - set.Remove value - - let inline difference values set : HashSet<_> = - check set - values |> set.Except - - let inline clear set : HashSet<_> = - check set - set.Clear () - - let inline filter predicate set = set |> Seq.filter predicate |> empty.Union - - let inline where predicate set = set |> Seq.where predicate |> empty.Union - - let inline pick chooser set = - check set - set |> Seq.pick chooser - - let inline tryPick chooser set = - check set - set |> Seq.tryPick chooser - - let inline vTryPick chooser set = - check set - - match set |> Seq.tryPick chooser with - | Some value -> ValueSome value - | None -> ValueNone - - let inline map mapping (set : HashSet<_>) = set |> Seq.map mapping |> ofSeq - - let inline forall predicate set = set |> Seq.forall predicate - - let inline iter action (set : HashSet<_>) = - check set - set |> Seq.iter action - type SortedSet<'T> = System.Collections.Immutable.ImmutableSortedSet<'T> type SortedSetBuilder<'T> = SortedSet<'T>.Builder diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetContract.fs new file mode 100644 index 00000000..99553618 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetContract.fs @@ -0,0 +1,404 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The helpers that the contract bases of the set modules share. +/// +/// +/// +/// Every set module mirrors one FSharp.Core module, Set (ADR 0001, D6). The base therefore compares the exceptions +/// with the Set oracle through ExceptionAssert.Same and needs no case of Mirror, which keeps the +/// contracts of the sequence-like modules unchanged. +/// +/// +/// The order in which a hash set enumerates its elements is unspecified, so the contracts compare the elements of a set +/// as a sorted array, the order of an F# set. +/// +/// +/// The name of the module under test, for the messages of the assertions. +/// Maps the name of a parameter of the FSharp.Core oracle to the name of the parameter of the module. +/// Creates a set of the given elements. +/// The elements in the order in which the set enumerates them. +[] +type SetContractBase<'S> (name : string, paramName : string -> string, ofSeq : seq -> 'S, toArray : 'S -> int array) = + + /// The elements of a set, sorted. + member _.Sorted (set : 'S) = Array.sort (toArray set) + + /// The set of the elements of an input and the F# set of the same elements, the oracle. + member _.Create (input : int array) = struct (ofSeq input, Set.ofArray input) + + /// Asserts that a function of the module throws what the same function of the FSharp.Core `Set` module throws. + member _.ThrowsLikeSet (oracle : unit -> 'O, actual : unit -> 'R, message : string) = + ExceptionAssert.Same (paramName, oracle >> ignore, actual >> ignore, $"%s{name}: %s{message}") + + /// + /// Asserts that a function of the module throws for a null input that the + /// FSharp.Core Set function dereferences without a check. + /// + /// The name of the parameter in the signature of the FSharp.Core function. + /// The call of the function of the module. + /// The message of the assertion. + member _.ThrowsArgumentNull (setParamName : string, actual : unit -> 'R, message : string) = + let ex = + Assert.ThrowsExactly(Action (actual >> ignore), $"%s{name}: %s{message}") + + Assert.AreEqual(paramName setParamName, ex.ParamName, $"%s{name}: %s{message} names the parameter") + +/// +/// The contract of the reading functions that every set module provides: HashSet, SortedSet, ISet and +/// FrozenSet (ADR 0001, Appendix C). +/// +[] +type SetContract<'S> (ops : SetOps<'S>) = + inherit SetContractBase<'S> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// 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 |] + + /// + /// The sets of a pair of inputs for the relations under the comparer of the first set. + /// + /// + /// The first set compares by remainders and the second one with the default comparer, so that elements that the second + /// set tells apart can be one element for the first set (ADR 0001, D14). + /// + let createByRemainder (first : int array) (second : int array) = struct (ops.OfSeqByRemainder first, ops.OfSeq second) + + /// + /// Every input with each predicate of exists and forall, as rows: one that some elements satisfy, one that few or no + /// elements satisfy, one that no element satisfies and one that every element satisfies. + /// + static member InputsAndPredicates = seq { + for input in SetInputs.All do + for predicate in + [| + ElementPredicate.IsEven + ElementPredicate.IsNegative + ElementPredicate.Never + ElementPredicate.IsBelowHundred + |] do + struct (input, predicate) + } + + // ---- Properties ---- + + [)>] + member this.``count counts the distinct elements`` (input : int array) = + let struct (set, expected) = this.Create input + Assert.AreEqual(Set.count expected, ops.Count set, $"%s{ops.Name}.count must count the distinct elements") + + [)>] + member this.``isEmpty is true only for a set without elements`` (input : int array) = + let struct (set, expected) = this.Create input + + Assert.AreEqual( + Set.isEmpty expected, + ops.IsEmpty set, + $"%s{ops.Name}.isEmpty must be true only for a set without elements" + ) + + // ---- Access ---- + + [)>] + member this.``tryGetValue returns the element equal to the value or nothing`` (input : int array, value : int) = + let struct (set, expected) = this.Create input + + Assert.AreEqual( + (if Set.contains value expected then + ValueSome value + else + ValueNone), + ops.TryGetValue value set, + $"%s{ops.Name}.tryGetValue must return the element equal to the value or nothing" + ) + + // ---- Search ---- + + [)>] + member this.``contains finds an element equal to the value`` (input : int array, value : int) = + let struct (set, expected) = this.Create input + + Assert.AreEqual( + Set.contains value expected, + ops.Contains value set, + $"%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 (set, expected) = this.Create input + + Assert.AreEqual( + Set.exists predicate.Matches expected, + ops.Exists predicate.Matches set, + $"%s{ops.Name}.exists must find an element that satisfies the predicate" + ) + + [] + member _.``exists stops at the first element that satisfies the predicate`` () = + let set = ops.OfSeq Inputs.Several + let mutable calls = 0 + + let found = + ops.Exists + (fun _ -> + calls <- calls + 1 + true + ) + set + + Assert.IsTrue (found, $"%s{ops.Name}.exists must find an element") + Assert.AreEqual(1, calls, $"%s{ops.Name}.exists must stop at the first element that satisfies the predicate") + + [.InputsAndPredicates)>] + member this.``forall tests every element`` (input : int array, predicate : ElementPredicate) = + let struct (set, expected) = this.Create input + + Assert.AreEqual( + Set.forall predicate.Matches expected, + ops.Forall predicate.Matches set, + $"%s{ops.Name}.forall must test every element" + ) + + [] + member _.``forall stops at the first element that does not satisfy the predicate`` () = + let set = ops.OfSeq Inputs.Several + let mutable calls = 0 + + let satisfied = + ops.Forall + (fun _ -> + calls <- calls + 1 + false + ) + set + + Assert.IsFalse (satisfied, $"%s{ops.Name}.forall must find an element that does not satisfy the predicate") + Assert.AreEqual(1, calls, $"%s{ops.Name}.forall must stop at the first element that does not satisfy the predicate") + + [)>] + member this.``isProperSubset tests whether the first set is a proper subset of the second one`` + (first : int array, second : int array) + = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + Set.isProperSubset expected1 expected2, + ops.IsProperSubset set1 set2, + $"%s{ops.Name}.isProperSubset must test whether the first set is a proper subset of the second one" + ) + + [)>] + member this.``isProperSuperset tests whether the first set is a proper superset of the second one`` + (first : int array, second : int array) + = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + Set.isProperSuperset expected1 expected2, + ops.IsProperSuperset set1 set2, + $"%s{ops.Name}.isProperSuperset must test whether the first set is a proper superset of the second one" + ) + + [)>] + member this.``isSubset tests whether the first set is a subset of the second one`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + Set.isSubset expected1 expected2, + ops.IsSubset set1 set2, + $"%s{ops.Name}.isSubset must test whether the first set is a subset of the second one" + ) + + [)>] + member this.``isSuperset tests whether the first set is a superset of the second one`` + (first : int array, second : int array) + = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + Set.isSuperset expected1 expected2, + ops.IsSuperset set1 set2, + $"%s{ops.Name}.isSuperset must test whether the first set is a superset of the second one" + ) + + [)>] + member this.``overlaps tests whether the sets have an element in common`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + not (Set.isEmpty (Set.intersect expected1 expected2)), + ops.Overlaps set1 set2, + $"%s{ops.Name}.overlaps must test whether the sets have an element in common" + ) + + [)>] + member this.``setEquals tests whether the sets have the same elements`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + Assert.AreEqual( + (expected1 = expected2), + ops.SetEquals set1 set2, + $"%s{ops.Name}.setEquals must test whether the sets have the same elements" + ) + + [)>] + member _.``isProperSubset compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + Set.isProperSubset (SetInputs.Remainders first) (SetInputs.Remainders second), + ops.IsProperSubset set1 set2, + $"%s{ops.Name}.isProperSubset must count the elements that the comparer of the first set finds equal once" + ) + + [)>] + member _.``isProperSuperset compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + Set.isProperSuperset (SetInputs.Remainders first) (SetInputs.Remainders second), + ops.IsProperSuperset set1 set2, + $"%s{ops.Name}.isProperSuperset must count the elements that the comparer of the first set finds equal once" + ) + + [)>] + member _.``isSubset compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + Set.isSubset (SetInputs.Remainders first) (SetInputs.Remainders second), + ops.IsSubset set1 set2, + $"%s{ops.Name}.isSubset must count the elements that the comparer of the first set finds equal once" + ) + + [)>] + member _.``isSuperset compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + Set.isSuperset (SetInputs.Remainders first) (SetInputs.Remainders second), + ops.IsSuperset set1 set2, + $"%s{ops.Name}.isSuperset must count the elements that the comparer of the first set finds equal once" + ) + + [)>] + member _.``overlaps compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + not (Set.isEmpty (Set.intersect (SetInputs.Remainders first) (SetInputs.Remainders second))), + ops.Overlaps set1 set2, + $"%s{ops.Name}.overlaps must compare in the comparer of the first set" + ) + + [)>] + member _.``setEquals compares in the comparer of the first set`` (first : int array, second : int array) = + let struct (set1, set2) = createByRemainder first second + + Assert.AreEqual( + (SetInputs.Remainders first = SetInputs.Remainders second), + ops.SetEquals set1 set2, + $"%s{ops.Name}.setEquals must count the elements that the comparer of the first set finds equal once" + ) + + // ---- Folding ---- + + [)>] + member this.``fold visits every element once in the order of enumeration`` (input : int array) = + let struct (set, expected) = this.Create input + let visited = ops.Fold record [||] set + + CollectionAssert.AreEqual ( + Array.fold record [||] (ops.ToArray set), + visited, + $"%s{ops.Name}.fold must visit the elements in the order of enumeration" + ) + + CollectionAssert.AreEqual (Set.toArray expected, Array.sort visited, $"%s{ops.Name}.fold must visit every element once") + + [)>] + member this.``foldBack visits every element once in the reverse order of enumeration`` (input : int array) = + let struct (set, expected) = this.Create input + let visited = ops.FoldBack recordBack set [||] + + CollectionAssert.AreEqual ( + Array.foldBack recordBack (ops.ToArray set) [||], + visited, + $"%s{ops.Name}.foldBack must visit the elements in the reverse order of enumeration" + ) + + CollectionAssert.AreEqual ( + Set.toArray expected, + Array.sort visited, + $"%s{ops.Name}.foldBack must visit every element once" + ) + + [)>] + member this.``iter visits every element once in the order of enumeration`` (input : int array) = + let struct (set, _) = this.Create input + let visited = ResizeArray() + ops.Iter visited.Add set + + CollectionAssert.AreEqual ( + ops.ToArray set, + visited.ToArray (), + $"%s{ops.Name}.iter must visit every element once in the order of enumeration" + ) + + // ---- Conversion ---- + + [)>] + member this.``toArray returns every element of the input once`` (input : int array) = + // The other tests observe a set with toArray, so toArray is checked against the sequence that the set was created + // from, whose distinct elements a set holds + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted (ops.OfSeq input), + $"%s{ops.Name}.toArray must return every distinct element of the sequence that the set was created from once" + ) + + [] + member this.``toArray returns a new array`` () = + let set = ops.OfSeq Inputs.Several + let array = ops.ToArray set + array[0] <- 1000 + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + this.Sorted set, + $"%s{ops.Name}.toArray must return a copy that does not change the set" + ) + + [)>] + member this.``toList returns the elements in the order of enumeration`` (input : int array) = + let struct (set, _) = this.Create input + + CollectionAssert.AreEqual ( + ops.ToArray set, + List.toArray (ops.ToList set), + $"%s{ops.Name}.toList must return the elements in the order in which the set enumerates them" + ) + + [)>] + member this.``toSeq returns the elements in the order of enumeration`` (input : int array) = + let struct (set, _) = this.Create input + + CollectionAssert.AreEqual ( + ops.ToArray set, + Seq.toArray (ops.ToSeq set), + $"%s{ops.Name}.toSeq must return the elements in the order in which the set enumerates them" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetCreationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetCreationContract.fs new file mode 100644 index 00000000..fd2206d8 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetCreationContract.fs @@ -0,0 +1,210 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the creation functions of the concrete set modules, HashSet, SortedSet and +/// FrozenSet (ADR 0001, Appendix C), and of the members of the struct that creates their sets with a comparer (D12). +/// +/// +/// A set is a sequence of its elements, so the struct can give an existing set its comparer: the contract needs the set +/// type to be a sequence of numbers for that check. +/// +[] +type SetCreationContract<'S when 'S :> seq> (ops : SetCreationOps<'S>) = + inherit SetContractBase<'S> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + let custom = ops.CustomComparer + + /// The struct in the messages of the assertions. + let customName = $"%s{ops.Name}.forComparer" + + /// Asserts that a set compares with the comparer of the struct and holds one element of the input for each remainder, + /// the classes of the comparer. + let holdsOnePerRemainder (input : int array) (set : 'S) (message : string) = + Assert.AreSame (custom.Comparer, ops.Comparer set, $"%s{message} must compare with the comparer of the struct") + let elements = ops.ToArray set + + CollectionAssert.AreEqual ( + input + |> Array.map SetInputs.Remainder + |> Array.distinct + |> Array.sort, + elements |> Array.map SetInputs.Remainder |> Array.sort, + $"%s{message} must hold one element for each remainder" + ) + + for element in elements do + Assert.Contains (element, input, $"%s{message} must hold elements of the input") + + /// Creates a set from a copy of an input and changes the copy afterwards, so that a set that shared the memory of its + /// source would show the change. + let createFromChangedCopy (create : int array -> 'S) (input : int array) = + let source = Array.copy input + let set = create source + + if source.Length > 0 then + source[0] <- 1000 + + set + + // ---- Creation ---- + + [] + member this.``empty has no elements`` () = + Assert.IsEmpty (this.Sorted (ops.Empty ()), $"%s{ops.Name}.empty must have no elements") + + [)>] + member this.``ofArray copies the distinct elements`` (input : int array) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted (createFromChangedCopy ops.OfArray input), + $"%s{ops.Name}.ofArray must copy the distinct elements" + ) + + [] + member this.``ofArray throws for a null array as FSharp.Core does`` () = + let source = Unchecked.defaultof + + this.ThrowsLikeSet ((fun () -> Set.ofArray source), (fun () -> ops.OfArray source), "ofArray of a null array must throw") + + [)>] + member this.``ofList keeps the distinct elements`` (input : int array) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted (ops.OfList (List.ofArray input)), + $"%s{ops.Name}.ofList must keep the distinct elements" + ) + + // Set.ofList dereferences a null list without a check, so the function throws ArgumentNullException instead (D6) + [] + member this.``ofList throws ArgumentNullException for a null list`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> ops.OfList (Unchecked.defaultof)), + "ofList of a null list must throw" + ) + + [)>] + member this.``ofReadOnlySpan copies the distinct elements`` (input : int array) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted (createFromChangedCopy ops.OfReadOnlySpan input), + $"%s{ops.Name}.ofReadOnlySpan must copy the distinct elements" + ) + + [)>] + member this.``ofSeq keeps the distinct elements`` (input : int array) = + let expected = Set.toArray (Set.ofArray input) + + CollectionAssert.AreEqual ( + expected, + this.Sorted (ops.OfSeq (List.ofArray input)), + $"%s{ops.Name}.ofSeq of a list must keep the distinct elements" + ) + + CollectionAssert.AreEqual ( + expected, + this.Sorted (ops.OfSeq input), + $"%s{ops.Name}.ofSeq of an array must keep the distinct elements" + ) + + CollectionAssert.AreEqual ( + expected, + this.Sorted (ops.OfSeq (Seq.map id input)), + $"%s{ops.Name}.ofSeq of a lazy sequence must keep the distinct elements" + ) + + // Set.ofSeq dereferences a null sequence without a check, so the function throws ArgumentNullException instead (D6) + [] + member this.``ofSeq throws ArgumentNullException for a null sequence`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> ops.OfSeq (Unchecked.defaultof>)), + "ofSeq of a null sequence must throw" + ) + + [)>] + member this.``ofSpan copies the distinct elements`` (input : int array) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted (createFromChangedCopy ops.OfSpan input), + $"%s{ops.Name}.ofSpan must copy the distinct elements" + ) + + [] + member this.``singleton has one element`` () = + CollectionAssert.AreEqual ([| 42 |], this.Sorted (ops.Singleton 42), $"%s{ops.Name}.singleton must have one element") + + // ---- The struct of forComparer ---- + + [] + member _.``the struct creates an empty set with its comparer`` () = + let set = custom.Empty () + Assert.IsEmpty (set, $"%s{customName}.empty must have no elements") + Assert.AreSame (custom.Comparer, ops.Comparer set, $"%s{customName}.empty must compare with the comparer of the struct") + + [)>] + member _.``the struct copies one element of the array for each class of its comparer`` (input : int array) = + holdsOnePerRemainder input (createFromChangedCopy custom.OfArray input) $"%s{customName}.ofArray" + + [] + member this.``the struct throws for a null array as FSharp.Core does`` () = + let source = Unchecked.defaultof + + this.ThrowsLikeSet ( + (fun () -> Set.ofArray source), + (fun () -> custom.OfArray source), + "forComparer ofArray of a null array must throw" + ) + + [)>] + member _.``the struct keeps one element of the list for each class of its comparer`` (input : int array) = + holdsOnePerRemainder input (custom.OfList (List.ofArray input)) $"%s{customName}.ofList" + + [] + member this.``the struct throws ArgumentNullException for a null list`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> custom.OfList (Unchecked.defaultof)), + "forComparer ofList of a null list must throw" + ) + + [)>] + member _.``the struct copies one element of the read-only span for each class of its comparer`` (input : int array) = + holdsOnePerRemainder input (createFromChangedCopy custom.OfReadOnlySpan input) $"%s{customName}.ofReadOnlySpan" + + [)>] + member _.``the struct keeps one element of the sequence for each class of its comparer`` (input : int array) = + holdsOnePerRemainder input (custom.OfSeq (List.ofArray input)) $"%s{customName}.ofSeq of a list" + holdsOnePerRemainder input (custom.OfSeq input) $"%s{customName}.ofSeq of an array" + holdsOnePerRemainder input (custom.OfSeq (Seq.map id input)) $"%s{customName}.ofSeq of a lazy sequence" + + [)>] + member this.``the struct gives an existing set its comparer`` (input : int array) = + let existing = ops.OfSeq input + + holdsOnePerRemainder input (custom.OfSeq (existing :> seq)) $"%s{customName}.ofSeq of a set" + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray input), + this.Sorted existing, + $"%s{customName}.ofSeq must not change the set" + ) + + [] + member this.``the struct throws ArgumentNullException for a null sequence`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> custom.OfSeq (Unchecked.defaultof>)), + "forComparer ofSeq of a null sequence must throw" + ) + + [)>] + member _.``the struct copies one element of the span for each class of its comparer`` (input : int array) = + holdsOnePerRemainder input (createFromChangedCopy custom.OfSpan input) $"%s{customName}.ofSpan" + + [] + member _.``the struct creates a set of one element with its comparer`` () = + holdsOnePerRemainder [| 42 |] (custom.Singleton 42) $"%s{customName}.singleton" diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetInputs.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetInputs.fs new file mode 100644 index 00000000..a4551ece --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetInputs.fs @@ -0,0 +1,214 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic + +/// +/// The inputs that the set contracts run the functions on, and the comparers of the structs that create sets with a +/// comparer (ADR 0001, D12). +/// +/// +/// Every property builds new arrays on each read: MSTest hands the arrays of a row source to the tests as they are, so an +/// array that the tests shared would carry the change that one test made into the others. +/// +[] +type SetInputs = + + static let sameRemainder : IEqualityComparer = { + new IEqualityComparer with + member _.Equals (left, right) = SetInputs.Remainder left = SetInputs.Remainder right + member _.GetHashCode item = SetInputs.Remainder item + } + + static let byRemainder : IComparer = { + new IComparer with + member _.Compare (left, right) = compare (SetInputs.Remainder left) (SetInputs.Remainder right) + } + + /// A subset of `Inputs.Several` that overlaps `Inputs.Other`, for the subset and superset predicates. + static member Subset = [| 5; 8 |] + + /// + /// Every input, as the rows of the tests that run on each one: no elements, one element, elements with duplicates, + /// an input that overlaps it and a subset of it. + /// + static member All = seq { + Inputs.Empty + Inputs.Singleton + Inputs.Several + Inputs.Other + SetInputs.Subset + } + + /// + /// Every ordered pair of inputs, as the rows of the functions of two sets: equal, disjoint, overlapping, subsets and + /// supersets. + /// + static member Pairs = seq { + for first in SetInputs.All do + for second in SetInputs.All do + struct (first, second) + } + + /// + /// The pairs of inputs whose second input is a subset of the first one, as rows: the second input adds nothing to the + /// first one, and empty and equal inputs are among them. + /// + static member SubsetPairs = + SetInputs.Pairs + |> Seq.filter (fun struct (first, second) -> Set.isSubset (Set.ofArray second) (Set.ofArray first)) + + /// + /// The pairs of inputs whose remainders in the division by 5 are all remainders of the first input too, as rows: the + /// second input adds nothing to a set that compares by remainders, although it may hold other numbers. + /// + static member SubsetPairsByRemainder = + SetInputs.Pairs + |> Seq.filter (fun struct (first, second) -> Set.isSubset (SetInputs.Remainders second) (SetInputs.Remainders first)) + + /// The pairs of inputs that have no number in common, as rows: the second input removes nothing from the first one. + static member DisjointPairs = + SetInputs.Pairs + |> Seq.filter (fun struct (first, second) -> Set.isEmpty (Set.intersect (Set.ofArray first) (Set.ofArray second))) + + /// + /// The pairs of inputs that have no remainder in the division by 5 in common, as rows: the second input removes nothing + /// from a set that compares by remainders, although it may hold numbers of the first input. + /// + static member DisjointPairsByRemainder = + SetInputs.Pairs + |> Seq.filter (fun struct (first, second) -> + Set.isEmpty (Set.intersect (SetInputs.Remainders first) (SetInputs.Remainders second)) + ) + + /// The values to look up: in no input, in one input and in several inputs. + static member Values = [| 42; 5; -3; 8; 1000 |] + + /// Every input with every value to look up, add or remove, as rows. + static member InputsAndValues = seq { + for input in SetInputs.All do + for value in SetInputs.Values do + struct (input, value) + } + + /// + /// The remainder of a number in the division by 5, from 0 to 4. + /// + /// + /// The comparers of the structs that create sets with a comparer consider two numbers equal when their remainders are + /// equal, so that they merge elements of the inputs that the default comparer tells apart: 5 and 0, or -3, 12 and 7 + /// of Inputs.Several. + /// + static member Remainder (item : int) = ((item % 5) + 5) % 5 + + /// The remainders of the numbers of an input in the division by 5, as a set: the classes of the numbers for a set that + /// compares by remainders. + static member Remainders (input : int array) = Set.ofArray (Array.map SetInputs.Remainder input) + + /// The equality comparer of the structs that create hash and frozen sets: equal remainders make equal numbers. + static member SameRemainder = sameRemainder + + /// The comparer of the structs that create sorted sets: it orders numbers by their remainders. + static member ByRemainder = byRemainder + +/// +/// The inputs of the tests that pass a sequence to a relation or to an operation of the set algebra. The functions of the +/// set modules take sets, where the members of the BCL types take sequences, so a sequence is converted to a set first +/// (ADR 0001, D14). +/// +/// +/// +/// A test builds the first set from the strings of a relation with one of the comparers, builds the sequence from the +/// other strings, and compares the answer of the member of the BCL type for the sequence with the answer of the function of +/// the module for the set that the sequence is converted to. +/// +/// +/// The strings tell the comparers apart: when the first set compares by reference, each relation and each operation answers +/// differently for the sequence that the default comparer converts, which keeps one of "a1" and "a2", than for the sequence +/// that the comparer of the first set converts. +/// +/// +[] +type SetRelationInputs = + + /// + /// The strings by name: "a1" and "a2" have the text "a" and are different instances, "A" differs from them in case, and + /// "b" has other text. + /// + /// + /// The instances are created once: a comparer by reference tells "a1" from "a2", so the first set and the sequence of a + /// test must hold the same two instances. + /// + static let pool : Map = + Map.ofArray [| "a1", String [| 'a' |]; "a2", String [| 'a' |]; "A", "A"; "b", "b" |] + + /// + /// The comparers of the first set, by name, each with whether it finds equal every pair of strings that the default + /// comparer finds equal. + /// + static let comparers : Map * bool)> = + Map.ofArray [| + "default comparer", struct (EqualityComparer.Default :> IEqualityComparer, true) + "comparer that ignores case", struct (StringComparer.OrdinalIgnoreCase :> IEqualityComparer, true) + // It tells "a1" from "a2", which the default comparer finds equal + "reference comparer", struct (HashIdentity.Reference, false) + |] + + /// + /// The names of the strings of the first set and of the sequence of each relation, by the name of the relation. + /// + static let items : Map = + Map.ofArray [| + "isProperSubset", struct ([| "a1"; "a2" |], [| "a1"; "a2"; "b" |]) + "isProperSuperset", struct ([| "a1"; "a2"; "A" |], [| "a1"; "a2"; "A" |]) + "isSubset", struct ([| "a1"; "a2"; "A" |], [| "a1"; "a2"; "A" |]) + "isSuperset", struct ([| "a1" |], [| "a1"; "a2" |]) + // The default comparer keeps "a2", the first string of the sequence + "overlaps", struct ([| "a1" |], [| "a2"; "a1" |]) + "setEquals", struct ([| "a1"; "a2"; "A" |], [| "a1"; "a2"; "A" |]) + |] + + /// + /// The names of the strings of the first set and of the sequence of each operation of the set algebra that takes a + /// sequence in the BCL types and returns a set, by the name of the operation. + /// + static let operationItems : Map = + Map.ofArray [| + "intersect", struct ([| "a1"; "a2" |], [| "a1"; "a2" |]) + "symmetricDifference", struct ([| "a1"; "a2" |], [| "a1"; "a2" |]) + |] + + /// The strings for their names in the pool. + static member Strings (names : string array) = names |> Array.map (fun name -> pool[name]) + + /// The names of the strings of the first set and of the sequence of each relation, by the name of the relation. + static member Items = items + + /// The comparers of the first set, by name, each with whether it finds equal every pair of strings that the default + /// comparer finds equal. + static member Comparers = comparers + + /// The relations of a set with a sequence, each with every comparer of the first set, as rows. + static member Rows = seq { + for relation in items.Keys do + for comparer in comparers.Keys do + struct (relation, comparer) + } + + /// The relations that need no conversion, each with every comparer of the first set, as rows. + static member StreamingRows = seq { + for relation in [ "isSuperset"; "overlaps" ] do + for comparer in comparers.Keys do + struct (relation, comparer) + } + + /// The names of the strings of the first set and of the sequence of each operation of the set algebra, by the name of + /// the operation. + static member OperationItems = operationItems + + /// The operations of the set algebra with a sequence, each with every comparer of the first set, as rows. + static member OperationRows = seq { + for operation in operationItems.Keys do + for comparer in comparers.Keys do + struct (operation, comparer) + } diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetMutationContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetMutationContract.fs new file mode 100644 index 00000000..4085908e --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetMutationContract.fs @@ -0,0 +1,212 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The contract of the incremental changes of the set modules that have them: HashSet, SortedSet and +/// ISet (ADR 0001, Appendix C). +/// +[] +type SetMutationContract<'S> (ops : SetMutationOps<'S>) = + inherit SetContractBase<'S> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// An input in the shapes that a sequence argument takes, each with its name for the message of the assertion: an + /// array, a list and a lazy sequence. + static let shapes (input : int array) = [| + struct ("an array", input :> seq) + struct ("a list", List.ofArray input :> seq) + struct ("a lazy sequence", Seq.map id input) + |] + + [)>] + member this.``add adds a value that the set does not contain`` (input : int array, value : int) = + let struct (set, expected) = this.Create input + + CollectionAssert.AreEqual ( + Set.toArray (Set.add value expected), + this.Sorted (ops.Add value set), + $"%s{ops.Name}.add must add a value that the set does not contain" + ) + + [] + member this.``add leaves the input unchanged`` () = + let set = ops.OfSeq Inputs.Several + ops.Add 1000 set |> ignore + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + this.Sorted set, + $"%s{ops.Name}.add must not change its input" + ) + + [)>] + member this.``addRange adds the elements that the set does not contain`` (first : int array, second : int array) = + let struct (set, expected) = this.Create first + let expected = Set.toArray (Set.union expected (Set.ofArray second)) + + for struct (shape, elements) in shapes second do + CollectionAssert.AreEqual ( + expected, + this.Sorted (ops.AddRange elements set), + $"%s{ops.Name}.addRange of %s{shape} must add the elements that the set does not contain" + ) + + // The elements of the set stay, and of the elements that are new to it the first one of each class is added (D14) + [)>] + member this.``addRange keeps the elements of the set that the comparer finds equal`` (first : int array, second : int array) = + let set = ops.OfSeqByRemainder first + let held = ops.ToArray set + let classes = SetInputs.Remainders held + + let added = + second + |> Array.filter (fun item -> not (Set.contains (SetInputs.Remainder item) classes)) + |> Array.distinctBy SetInputs.Remainder + + for struct (shape, elements) in shapes second do + CollectionAssert.AreEqual ( + Array.sort (Array.append held added), + this.Sorted (ops.AddRange elements set), + $"%s{ops.Name}.addRange of %s{shape} must keep the elements of the set and add the first of the other elements of each remainder" + ) + + [] + member this.``addRange leaves the input unchanged`` () = + let set = ops.OfSeq Inputs.Several + ops.AddRange Inputs.Other set |> ignore + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + this.Sorted set, + $"%s{ops.Name}.addRange must not change its input" + ) + + [)>] + member _.``addRange returns the input when the sequence adds no element`` (first : int array, second : int array) = + let set = ops.OfSeq first + + Assert.AreSame ( + box set, + box (ops.AddRange second set), + $"%s{ops.Name}.addRange must return the input when the sequence adds no element" + ) + + [)>] + member _.``addRange returns the input when the comparer finds every element of the sequence in the set`` + (first : int array, second : int array) + = + let set = ops.OfSeqByRemainder first + + Assert.AreSame ( + box set, + box (ops.AddRange second set), + $"%s{ops.Name}.addRange must return the input when the comparer finds every element of the sequence in the set" + ) + + [] + member this.``addRange throws ArgumentNullException for a null sequence`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> ops.AddRange (Unchecked.defaultof>) (ops.OfSeq Inputs.Several)), + "addRange of a null sequence must throw" + ) + + [)>] + member this.``clear removes every element`` (input : int array) = + let struct (set, _) = this.Create input + Assert.IsEmpty (this.Sorted (ops.Clear set), $"%s{ops.Name}.clear must remove every element") + + [)>] + member this.``remove removes the element equal to the value`` (input : int array, value : int) = + let struct (set, expected) = this.Create input + + CollectionAssert.AreEqual ( + Set.toArray (Set.remove value expected), + this.Sorted (ops.Remove value set), + $"%s{ops.Name}.remove must remove the element equal to the value" + ) + + [] + member this.``remove leaves the input unchanged`` () = + let set = ops.OfSeq Inputs.Several + ops.Remove 5 set |> ignore + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + this.Sorted set, + $"%s{ops.Name}.remove must not change its input" + ) + + [)>] + member this.``removeRange removes the elements that the sequence contains`` (first : int array, second : int array) = + let struct (set, expected) = this.Create first + let expected = Set.toArray (Set.difference expected (Set.ofArray second)) + + for struct (shape, elements) in shapes second do + CollectionAssert.AreEqual ( + expected, + this.Sorted (ops.RemoveRange elements set), + $"%s{ops.Name}.removeRange of %s{shape} must remove the elements that the sequence contains" + ) + + // The comparer of the set decides which elements are equal to those of the sequence (D14) + [)>] + member this.``removeRange removes the elements that the comparer finds equal`` (first : int array, second : int array) = + let set = ops.OfSeqByRemainder first + let classes = SetInputs.Remainders second + + let remaining = + ops.ToArray set + |> Array.filter (fun item -> not (Set.contains (SetInputs.Remainder item) classes)) + |> Array.sort + + for struct (shape, elements) in shapes second do + CollectionAssert.AreEqual ( + remaining, + this.Sorted (ops.RemoveRange elements set), + $"%s{ops.Name}.removeRange of %s{shape} must remove the elements that the comparer finds equal to those of the sequence" + ) + + [] + member this.``removeRange leaves the input unchanged`` () = + let set = ops.OfSeq Inputs.Several + ops.RemoveRange Inputs.Other set |> ignore + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + this.Sorted set, + $"%s{ops.Name}.removeRange must not change its input" + ) + + [)>] + member _.``removeRange returns the input when the sequence has no element of the set`` + (first : int array, second : int array) + = + let set = ops.OfSeq first + + Assert.AreSame ( + box set, + box (ops.RemoveRange second set), + $"%s{ops.Name}.removeRange must return the input when the sequence has no element of the set" + ) + + [)>] + member _.``removeRange returns the input when the comparer finds no element of the sequence in the set`` + (first : int array, second : int array) + = + let set = ops.OfSeqByRemainder first + + Assert.AreSame ( + box set, + box (ops.RemoveRange second set), + $"%s{ops.Name}.removeRange must return the input when the comparer finds no element of the sequence in the set" + ) + + [] + member this.``removeRange throws ArgumentNullException for a null sequence`` () = + this.ThrowsArgumentNull ( + "elements", + (fun () -> ops.RemoveRange (Unchecked.defaultof>) (ops.OfSeq Inputs.Several)), + "removeRange of a null sequence must throw" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetOps.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetOps.fs new file mode 100644 index 00000000..e4dcdcba --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetOps.fs @@ -0,0 +1,163 @@ +namespace FSharp.Collections.Immutable.Tests + +/// +/// The reading functions of a set 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. Every set module has this facet: HashSet, SortedSet, +/// ISet and FrozenSet. +/// +/// +/// The oracle of every set module is the FSharp.Core Set module. The order in which a hash set enumerates its +/// elements is unspecified, so the contracts compare the elements of a set as a sorted array, the order of an F# set. +/// +/// +type SetOps<'S> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// 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 set of the given elements. + OfSeq : seq -> 'S + /// Creates a set of the given elements that compares them by their remainders in the division by 5: the struct of + /// `forComparer` with `SetInputs.SameRemainder`, or `SetInputs.ByRemainder` for a sorted set. + OfSeqByRemainder : seq -> 'S + /// The elements in the order in which the set enumerates them: the `toArray` of the module, which the conversion + /// tests check as well. + ToArray : 'S -> int array + + Count : 'S -> int + IsEmpty : 'S -> bool + + TryGetValue : int -> 'S -> int voption + + Contains : int -> 'S -> bool + Exists : (int -> bool) -> 'S -> bool + Forall : (int -> bool) -> 'S -> bool + IsProperSubset : 'S -> 'S -> bool + IsProperSuperset : 'S -> 'S -> bool + IsSubset : 'S -> 'S -> bool + IsSuperset : 'S -> 'S -> bool + Overlaps : 'S -> 'S -> bool + SetEquals : 'S -> 'S -> bool + + Fold : (int array -> int -> int array) -> int array -> 'S -> int array + FoldBack : (int -> int array -> int array) -> 'S -> int array -> int array + Iter : (int -> unit) -> 'S -> unit + + ToList : 'S -> int list + ToSeq : 'S -> seq +} + +/// +/// The transformations of a set module for the contract tests: every set module has them. +/// +/// +/// Results of another set type or element type are arrays: the instance of a module converts its results with its own +/// toArray, so that one record type serves every module. ISet, for example, maps to a hash set. +/// +type SetTransformOps<'S> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// 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 set of the given elements. + OfSeq : seq -> 'S + /// The elements in the order in which the set enumerates them. + ToArray : 'S -> int array + + Choose : (int -> int voption) -> 'S -> int array + Difference : 'S -> 'S -> 'S + Filter : (int -> bool) -> 'S -> 'S + Intersect : 'S -> 'S -> 'S + IntersectMany : seq<'S> -> 'S + Map : (int -> int) -> 'S -> int array + SymmetricDifference : 'S -> 'S -> 'S + Union : 'S -> 'S -> 'S + UnionMany : seq<'S> -> 'S + + Partition : (int -> bool) -> 'S -> struct ('S * 'S) + PartitionWith : (int -> Choice) -> 'S -> struct (int array * string array) +} + +/// +/// The incremental changes of a set module for the contract tests: every set module except FrozenSet, which is +/// built once (ADR 0001, D1). +/// +type SetMutationOps<'S> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// 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 set of the given elements. + OfSeq : seq -> 'S + /// Creates a set of the given elements that compares them by their remainders in the division by 5, as the field of + /// `SetOps` does: the contract states which of two elements that the comparer finds equal stays (ADR 0001, D14). + OfSeqByRemainder : seq -> 'S + /// The elements in the order in which the set enumerates them. + ToArray : 'S -> int array + + Add : int -> 'S -> 'S + AddRange : seq -> 'S -> 'S + Clear : 'S -> 'S + Remove : int -> 'S -> 'S + RemoveRange : seq -> 'S -> 'S +} + +/// +/// The members of the struct that creates the sets of a module with a comparer (ADR 0001, D12) for the creation contract: +/// CustomComparerHashSet, CustomComparerSortedSet and CustomComparerFrozenSet. +/// +/// +/// The struct is created with a comparer of SetInputs: SetInputs.SameRemainder for hash and frozen sets and +/// SetInputs.ByRemainder for sorted sets. Both consider two numbers equal when their remainders in the division by +/// 5 are equal, so the contract states its expectations in remainders, whatever the type of the comparer. +/// +type SetCustomComparerOps<'S> = { + /// The comparer that the struct returns from its Comparer property. + Comparer : obj + + Empty : unit -> 'S + OfArray : int array -> 'S + OfList : int list -> 'S + /// Creates a set from a read-only span over the array: a span cannot be an argument of a function type. + OfReadOnlySpan : int array -> 'S + OfSeq : seq -> 'S + /// Creates a set from a span over the array: a span cannot be an argument of a function type. + OfSpan : int array -> 'S + Singleton : int -> 'S +} + +/// +/// The creation functions of a concrete set module for the contract tests: HashSet, SortedSet and +/// FrozenSet. +/// +/// +/// The facet also runs the members of the struct that creates sets with a comparer, the struct that forComparer +/// of the module returns. The builders are not part of it, because FrozenSet has none. +/// +type SetCreationOps<'S> = { + /// The name of the module, for the messages of the assertions. + Name : string + /// 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 set enumerates them. + ToArray : 'S -> int array + /// The comparer of a set, as the comparer function of the module returns it. + Comparer : 'S -> obj + + Empty : unit -> 'S + OfArray : int array -> 'S + OfList : int list -> 'S + /// Creates a set from a read-only span over the array: a span cannot be an argument of a function type. + OfReadOnlySpan : int array -> 'S + OfSeq : seq -> 'S + /// Creates a set from a span over the array: a span cannot be an argument of a function type. + OfSpan : int array -> 'S + Singleton : int -> 'S + + /// The members of the struct that forComparer creates with the comparer of SetInputs. + CustomComparer : SetCustomComparerOps<'S> +} diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetParamNames.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetParamNames.fs new file mode 100644 index 00000000..56c266b9 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetParamNames.fs @@ -0,0 +1,16 @@ +/// The names of the parameters of the set modules. +[] +module FSharp.Collections.Immutable.Tests.SetParamNames + +/// +/// Maps the name of a parameter of the FSharp.Core Set module to the name in the set modules. +/// +/// +/// The set modules name their parameters as the Set module does. Set.intersectMany and +/// Set.unionMany call Seq.reduce and Seq.fold, which name the sequence of sets source; the +/// set modules name it sets, as the signatures of those two functions do. +/// +let ofFSharpCore (name : string) = + match name with + | "source" -> "sets" + | name -> name diff --git a/tests/FSharp.Collections.Immutable.Tests/Contracts/SetTransformContract.fs b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetTransformContract.fs new file mode 100644 index 00000000..95e9e070 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/Contracts/SetTransformContract.fs @@ -0,0 +1,269 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The predicates of the filter tests of the set modules, 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 SetFilterPredicate = + /// The positive elements. + | IsPositive + /// The even elements. + | IsEven + /// No element. + | Never + + /// Tells whether an element satisfies the predicate. + member this.Matches (item : int) = + match this with + | SetFilterPredicate.IsPositive -> item > 0 + | SetFilterPredicate.IsEven -> item % 2 = 0 + | SetFilterPredicate.Never -> false + +/// +/// The mappings of the map tests of the set modules, 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 SetMapping = + /// Ten times the element, which keeps distinct elements distinct. + | TimesTen + /// The remainder of the element in the division by 3, which merges elements. + | RemainderOfThree + /// Zero for every element, which merges all elements into one. + | Zero + + /// Maps an element. + member this.Apply (item : int) = + match this with + | SetMapping.TimesTen -> item * 10 + | SetMapping.RemainderOfThree -> item % 3 + | SetMapping.Zero -> 0 + +/// +/// The contract of the transformations that every set module provides: HashSet, SortedSet, ISet and +/// FrozenSet (ADR 0001, Appendix C). The set algebra takes its arguments in the order of FSharp.Core. +/// +[] +type SetTransformContract<'S> (ops : SetTransformOps<'S>) = + inherit SetContractBase<'S> (ops.Name, ops.ParamName, ops.OfSeq, ops.ToArray) + + /// A chooser that keeps the positive elements and makes some of them collide. + static let chooser (item : int) = if item > 0 then ValueSome (item % 5) else ValueNone + + /// The same chooser as `chooser`, for the FSharp.Core oracle. + static let chooserOption (item : int) = if item > 0 then Some (item % 5) else None + + /// Every input with each predicate of filter, as rows. + static member InputsAndFilterPredicates = seq { + for input in SetInputs.All do + for predicate in [| SetFilterPredicate.IsPositive; SetFilterPredicate.IsEven; SetFilterPredicate.Never |] do + struct (input, predicate) + } + + /// Every input with each mapping of map, as rows. + static member InputsAndMappings = seq { + for input in SetInputs.All do + for mapping in [| SetMapping.TimesTen; SetMapping.RemainderOfThree; SetMapping.Zero |] do + struct (input, mapping) + } + + /// + /// The sequences of inputs of the functions of many sets, as rows: one input, two overlapping inputs, three inputs, + /// and the empty input after and before another one. + /// + /// + /// A row that is an array of arrays is wrapped in a TestDataRow, because MSTest spreads an array of a reference + /// type into the parameters of the test. + /// + static member InputSequences = seq { + TestDataRow [| Inputs.Several |] + TestDataRow [| Inputs.Several; Inputs.Other |] + TestDataRow [| Inputs.Several; SetInputs.Subset; Inputs.Other |] + TestDataRow [| Inputs.Other; Inputs.Empty |] + TestDataRow [| Inputs.Empty; Inputs.Singleton |] + } + + // ---- Transformation ---- + + [)>] + member this.``choose keeps the distinct chosen values`` (input : int array) = + let struct (set, expected) = this.Create input + + CollectionAssert.AreEqual ( + expected + |> Set.toArray + |> Array.choose chooserOption + |> Array.distinct + |> Array.sort, + Array.sort (ops.Choose chooser set), + $"%s{ops.Name}.choose must keep the distinct chosen values" + ) + + [)>] + member this.``difference removes the elements of the second set from the first one`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + CollectionAssert.AreEqual ( + Set.toArray (Set.difference expected1 expected2), + this.Sorted (ops.Difference set1 set2), + $"%s{ops.Name}.difference must remove the elements of the second set from the first one" + ) + + [.InputsAndFilterPredicates)>] + member this.``filter keeps the elements that satisfy the predicate`` (input : int array, predicate : SetFilterPredicate) = + let struct (set, expected) = this.Create input + + CollectionAssert.AreEqual ( + Set.toArray (Set.filter predicate.Matches expected), + this.Sorted (ops.Filter predicate.Matches set), + $"%s{ops.Name}.filter must keep the elements that satisfy the predicate" + ) + + [)>] + member this.``intersect keeps the elements that are in both sets`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + CollectionAssert.AreEqual ( + Set.toArray (Set.intersect expected1 expected2), + this.Sorted (ops.Intersect set1 set2), + $"%s{ops.Name}.intersect must keep the elements that are in both sets" + ) + + [.InputSequences)>] + member this.``intersectMany keeps the elements that are in every set`` (inputs : int[][]) = + let sets = inputs |> Array.map (fun input -> ops.OfSeq input) + + CollectionAssert.AreEqual ( + Set.toArray (Set.intersectMany (Array.map Set.ofArray inputs)), + this.Sorted (ops.IntersectMany sets), + $"%s{ops.Name}.intersectMany must keep the elements that are in every set" + ) + + [] + member this.``intersectMany throws for an empty sequence as FSharp.Core does`` () = + this.ThrowsLikeSet ( + (fun () -> Set.intersectMany Seq.empty>), + (fun () -> ops.IntersectMany Seq.empty), + "intersectMany of no sets must throw" + ) + + [] + member this.``intersectMany throws for a null sequence as FSharp.Core does`` () = + this.ThrowsLikeSet ( + (fun () -> Set.intersectMany (Unchecked.defaultof>>)), + (fun () -> ops.IntersectMany (Unchecked.defaultof>)), + "intersectMany of a null sequence must throw" + ) + + [.InputsAndMappings)>] + member this.``map keeps the distinct results`` (input : int array, mapping : SetMapping) = + let struct (set, expected) = this.Create input + + CollectionAssert.AreEqual ( + Set.toArray (Set.map mapping.Apply expected), + Array.sort (ops.Map mapping.Apply set), + $"%s{ops.Name}.map must keep the distinct results" + ) + + [)>] + member this.``symmetricDifference keeps the elements that are in exactly one of the sets`` + (first : int array, second : int array) + = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + CollectionAssert.AreEqual ( + Set.toArray (Set.union (Set.difference expected1 expected2) (Set.difference expected2 expected1)), + this.Sorted (ops.SymmetricDifference set1 set2), + $"%s{ops.Name}.symmetricDifference must keep the elements that are in exactly one of the sets" + ) + + [)>] + member this.``union keeps the elements that are in either set`` (first : int array, second : int array) = + let struct (set1, expected1) = this.Create first + let struct (set2, expected2) = this.Create second + + CollectionAssert.AreEqual ( + Set.toArray (Set.union expected1 expected2), + this.Sorted (ops.Union set1 set2), + $"%s{ops.Name}.union must keep the elements that are in either set" + ) + + [.InputSequences)>] + member this.``unionMany keeps the elements that are in any set`` (inputs : int[][]) = + let sets = inputs |> Array.map (fun input -> ops.OfSeq input) + + CollectionAssert.AreEqual ( + Set.toArray (Set.unionMany (Array.map Set.ofArray inputs)), + this.Sorted (ops.UnionMany sets), + $"%s{ops.Name}.unionMany must keep the elements that are in any set" + ) + + [] + member this.``unionMany of no sets is empty`` () = + Assert.IsEmpty (this.Sorted (ops.UnionMany Seq.empty), $"%s{ops.Name}.unionMany of no sets must be empty") + + [] + member this.``unionMany throws for a null sequence as FSharp.Core does`` () = + this.ThrowsLikeSet ( + (fun () -> Set.unionMany (Unchecked.defaultof>>)), + (fun () -> ops.UnionMany (Unchecked.defaultof>)), + "unionMany of a null sequence must throw" + ) + + // ---- Partitioning ---- + + [)>] + member this.``partition splits the elements by the predicate`` (input : int array) = + let struct (set, expected) = this.Create input + let expectedAccepted, expectedRejected = Set.partition (fun item -> item > 0) expected + let struct (accepted, rejected) = ops.Partition (fun item -> item > 0) set + + CollectionAssert.AreEqual ( + Set.toArray expectedAccepted, + this.Sorted accepted, + $"%s{ops.Name}.partition must return the elements that satisfy the predicate first" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedRejected, + this.Sorted rejected, + $"%s{ops.Name}.partition must return the other elements second" + ) + + [)>] + member this.``partitionWith splits the elements by the choice`` (input : int array) = + let struct (set, expected) = this.Create input + + let partitioner (item : int) = + if item > 0 then + Choice1Of2 (item % 5) + else + Choice2Of2 (string (abs item % 2)) + + let expectedFirst, expectedSecond = Set.partitionWith partitioner expected + let struct (first, second) = ops.PartitionWith partitioner set + + CollectionAssert.AreEqual ( + Set.toArray expectedFirst, + Array.sort first, + $"%s{ops.Name}.partitionWith must return the values of the first choices first" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedSecond, + Array.sort second, + $"%s{ops.Name}.partitionWith must return the values of the second choices second" + ) 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 ff9cc792..acc60db8 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -49,6 +49,13 @@ + + + + + + + @@ -63,6 +70,18 @@ + + + + + + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetBuilderTests.fs new file mode 100644 index 00000000..343cfde1 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetBuilderTests.fs @@ -0,0 +1,162 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The builder functions of `HashSet` and the builder of its struct with a comparer; hash sets have no builder module +/// (ADR 0001, Appendix C). +[] +type HashSetBuilderTests () = + + static let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + static let defaultComparer : IEqualityComparer = EqualityComparer.Default + + /// Creates hash sets and builders that compare without case. + static let caseless = HashSet.forComparer ignoreCase + + /// The elements of a builder, sorted in the order of F# generic comparison. + static let sorted (elements : seq) = Seq.toArray elements |> Array.sort + + [] + member _.``build returns the hash set of the elements that the action added`` () = + let set = + HashSet.build (fun builder -> + builder.Add "b" |> ignore + builder.Add "a" |> ignore + builder.Add "b" |> ignore + ) + + CollectionAssert.AreEqual ([| "a"; "b" |], sorted set, "HashSet.build must hold the elements that the action added") + Assert.AreSame (defaultComparer, HashSet.comparer set, "HashSet.build must compare with the default comparer") + + [] + member _.``build hands an empty builder with the default comparer to the action`` () = + let set = + HashSet.build(fun builder -> + Assert.IsEmpty (builder, "HashSet.build must hand over an empty builder") + + Assert.AreSame ( + defaultComparer, + builder.KeyComparer, + "HashSet.build must hand over a builder that compares with the default comparer" + ) + ) + + Assert.IsEmpty (set, "HashSet.build of an action that adds nothing must return an empty hash set") + + [] + member _.``the build of the struct compares with its comparer`` () = + let set = + caseless.build (fun builder -> + Assert.AreSame ( + ignoreCase, + builder.KeyComparer, + "CustomComparerHashSet.build must hand over a builder that compares with its comparer" + ) + + builder.UnionWith [| "apple"; "Banana"; "APPLE" |] + ) + + CollectionAssert.AreEqual ( + [| "Banana"; "apple" |], + sorted set, + "CustomComparerHashSet.build must keep the first of the elements that the comparer considers equal" + ) + + Assert.AreSame (ignoreCase, HashSet.comparer set, "CustomComparerHashSet.build must keep the comparer") + + [] + member _.``builder starts empty with the default comparer`` () = + let builder = HashSet.builder() + Assert.IsEmpty (builder, "HashSet.builder must start without elements") + + Assert.AreSame (defaultComparer, builder.KeyComparer, "HashSet.builder must compare with the default comparer") + + [] + member _.``the builder of the struct compares with its comparer`` () = + let builder = caseless.builder () + Assert.IsEmpty (builder, "CustomComparerHashSet.builder must start without elements") + Assert.IsTrue (builder.Add "apple", "The builder must add the first element") + Assert.IsFalse (builder.Add "APPLE", "CustomComparerHashSet.builder must compare without case") + Assert.AreSame (ignoreCase, builder.KeyComparer, "CustomComparerHashSet.builder must keep the comparer") + + [] + member _.``ofBuilder returns the contents and keeps the builder usable`` () = + let builder = caseless.builder () + builder.UnionWith [| "apple"; "Banana" |] + let set = HashSet.ofBuilder builder + builder.Add "cherry" |> ignore + + CollectionAssert.AreEqual ([| "Banana"; "apple" |], sorted set, "HashSet.ofBuilder must hold the elements of the builder") + Assert.HasCount (3, builder, "HashSet.ofBuilder must keep the builder usable") + Assert.AreSame (ignoreCase, HashSet.comparer set, "HashSet.ofBuilder must keep the comparer of the builder") + + [] + member _.``toBuilder holds the elements and the comparer of the hash set`` () = + let set = caseless.ofArray [| "apple"; "Banana" |] + let builder = HashSet.toBuilder set + builder.Add "cherry" |> ignore + builder.Remove "APPLE" |> ignore + + Assert.AreSame (ignoreCase, builder.KeyComparer, "HashSet.toBuilder must keep the comparer") + CollectionAssert.AreEqual ([| "Banana"; "cherry" |], sorted builder, "HashSet.toBuilder must hold the elements") + CollectionAssert.AreEqual ([| "Banana"; "apple" |], sorted set, "HashSet.toBuilder must not change the hash set") + + [] + member _.``update changes a copy of the hash set`` () = + let set = caseless.ofArray [| "apple"; "Banana" |] + + let updated = + HashSet.update + (fun builder -> + builder.Add "cherry" |> ignore + builder.Remove "APPLE" |> ignore + ) + set + + CollectionAssert.AreEqual ([| "Banana"; "cherry" |], sorted updated, "HashSet.update must hold the changed elements") + CollectionAssert.AreEqual ([| "Banana"; "apple" |], sorted set, "HashSet.update must not change the hash set") + Assert.AreSame (ignoreCase, HashSet.comparer updated, "HashSet.update must keep the comparer") + + [] + member _.``update hands a builder with the elements and the comparer of the hash set to the action`` () = + let set = caseless.ofArray [| "apple"; "Banana" |] + + HashSet.update + (fun builder -> + CollectionAssert.AreEqual ( + [| "Banana"; "apple" |], + sorted builder, + "HashSet.update must hand over a builder with the elements of the hash set" + ) + + Assert.AreSame ( + ignoreCase, + builder.KeyComparer, + "HashSet.update must hand over a builder with the comparer of the hash set" + ) + ) + set + |> ignore + + // The in-place operations of the builder that no function of the module has + [] + member _.``update reaches the in-place operations of the builder`` () = + let set = HashSet.ofArray [| 1; 2; 3 |] + + CollectionAssert.AreEqual ( + [| 2; 3 |], + HashSet.toArray (HashSet.update (fun builder -> builder.IntersectWith [| 2; 3; 9 |]) set) + |> Array.sort, + "HashSet.update must apply IntersectWith" + ) + + CollectionAssert.AreEqual ( + [| 1; 3; 9 |], + HashSet.toArray (HashSet.update (fun builder -> builder.SymmetricExceptWith [| 2; 9 |]) set) + |> Array.sort, + "HashSet.update must apply SymmetricExceptWith" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetComputationExpressionTests.fs new file mode 100644 index 00000000..10bae15a --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetComputationExpressionTests.fs @@ -0,0 +1,259 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `hashSet { }` computation expression and the builders with a comparer. +[] +type HashSetComputationExpressionTests () = + + /// The elements of a hash set, sorted in the order of F# generic comparison. + static let sorted (set : HashSet<'T>) = HashSet.toArray set |> Array.sort + + /// + /// The expressions that reject their input, by name, with the parameter that names the input and the expression. + /// + /// + /// A default flat list converts to a sequence without a check and fails when the sequence is enumerated, and a null + /// input reaches UnionWith, which names its own parameter (ADR 0001, D6). Each expression rejects its input + /// when the buffer is created, not when the expression runs. + /// + static let rejections : Map unit))> = + let nullElements = Unchecked.defaultof> + let defaultElements = Unchecked.defaultof> :> seq + + // The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widenedElements = FlatListOracles.defaultStringsAsObjects + + Map.ofArray [| + "yield! (null sequence)", struct ("items", (fun () -> hashSet { yield! nullElements } |> ignore)) + "yield! (default flat list)", struct ("items", (fun () -> hashSet { yield! defaultElements } |> ignore)) + "yield! (default flat list of strings as objects)", + struct ("items", (fun () -> hashSet { yield! widenedElements } |> ignore)) + "yield! (null array)", struct ("items", (fun () -> hashSet { yield! Unchecked.defaultof } |> ignore)) + "yield! (null hash set)", + struct ("items", + (fun () -> + hashSet { yield! Unchecked.defaultof> } + |> ignore + )) + "for (null sequence)", + struct ("source", + (fun () -> + hashSet { + for item in nullElements do + yield item + } + |> ignore + )) + "for (default flat list)", + struct ("source", + (fun () -> + hashSet { + for item in defaultElements do + yield item + } + |> ignore + )) + "for (default flat list of strings as objects)", + struct ("source", + (fun () -> + hashSet { + for item in widenedElements do + yield item + } + |> ignore + )) + |] + + /// The names of the expressions that reject their input, as rows. + static member Rejections = rejections.Keys + + [] + member _.``an expression without elements is empty`` () = + let set : HashSet = hashSet { () } + Assert.IsTrue (HashSet.isEmpty set, "hashSet { () } must be empty") + + [] + member _.``yield adds each distinct element once`` () = + let set = hashSet { + yield 1 + yield 2 + yield 1 + } + + CollectionAssert.AreEqual ([| 1; 2 |], sorted set, "hashSet must add each distinct element once") + + [] + member _.``implicit yields add the elements`` () = + let set = hashSet { + 3 + 1 + 2 + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], sorted set, "hashSet must add the elements of implicit yields") + + [] + member _.``yield! adds a sequence, an array, a hash set and another expression`` () = + let inner = hashSet { yield 7 } + + let set = hashSet { + // A list is the sequence that the overload of sequences takes + yield! [ 1; 2 ] + yield! [| 3; 4 |] + yield! HashSet.ofArray [| 5; 6; 1 |] + yield! inner + } + + CollectionAssert.AreEqual ([| 1..7 |], sorted set, "hashSet must add the elements of yield!") + + [] + member _.``yield! and for reject a null input and a default flat list given as a sequence`` (name : string) = + let struct (paramName, actual) = rejections[name] + FlatListAssert.RejectsNull ($"hashSet {{ %s{name} }}", paramName, actual) + + [] + 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 set = hashSet { + yield! widened + + for item in widened do + yield item + } + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + HashSet.toArray set, + "hashSet must take the elements of a flat list of strings as objects in yield! and for" + ) + + [] + member _.``for adds the elements of the body for each item`` () = + let set = hashSet { + for item in -2 .. 2 do + yield item * item + } + + CollectionAssert.AreEqual ([| 0; 1; 4 |], sorted set, "hashSet must run the body of for for each item") + + [] + member _.``while adds the elements while the guard holds`` () = + let mutable count = 0 + + let set = hashSet { + while count < 3 do + count <- count + 1 + yield count + } + + CollectionAssert.AreEqual ([| 1; 2; 3 |], sorted set, "hashSet must run the body of while while the guard holds") + + [] + member _.``if without else and following elements combine`` () = + let set = hashSet { + if false then + yield 0 + + yield 1 + + if true then + yield 2 + } + + CollectionAssert.AreEqual ([| 1; 2 |], sorted set, "hashSet must combine conditional and plain elements") + + [] + member _.``try with adds the elements of the handler after an exception`` () = + let set = hashSet { + try + yield 1 + failwith "fails" + with _ -> + yield 2 + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + sorted set, + "hashSet must keep the elements before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let set = hashSet { + try + yield 1 + finally + compensated <- true + } + + Assert.IsTrue (compensated, "hashSet must run the compensation of try finally") + CollectionAssert.AreEqual ([| 1 |], sorted set, "hashSet must keep the elements of the body of try finally") + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let set = hashSet { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield 1 + } + + Assert.IsTrue (disposed, "hashSet must dispose the resource of use") + CollectionAssert.AreEqual ([| 1 |], sorted set, "hashSet must keep the elements of the body of use") + + [] + member _.``hashSet compares with the default comparer`` () = + let set = hashSet { + "apple" + "APPLE" + } + + Assert.AreSame ( + EqualityComparer.Default :> IEqualityComparer, + HashSet.comparer set, + "hashSet must compare with the default comparer" + ) + + Assert.HasCount (2, set, "hashSet must tell the case apart") + + [] + member _.``a builder with a comparer builds hash sets that compare with it`` () = + let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + let set = HashSetCE ignoreCase { + "apple" + "APPLE" + "Banana" + } + + Assert.AreSame (ignoreCase, HashSet.comparer set, "HashSetCE must build hash sets with its comparer") + CollectionAssert.AreEqual ( + [| "Banana"; "apple" |], + sorted set, + "HashSetCE must keep the first of the elements equal in its comparer" + ) + + [] + member _.``a builder rejects a null comparer when it is created`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + HashSetCE (Unchecked.defaultof>) + |> ignore + ), + "HashSetCE must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "HashSetCE must name the parameter comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetContractTests.fs new file mode 100644 index 00000000..56aab987 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetContractTests.fs @@ -0,0 +1,24 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The reading contract for `HashSet`. +[] +type HashSetSetTests () = + inherit SetContract> (HashSetOps.reading) + +/// The transformation contract for `HashSet`. +[] +type HashSetTransformTests () = + inherit SetTransformContract> (HashSetOps.transform) + +/// The mutation contract for `HashSet`. +[] +type HashSetMutationTests () = + inherit SetMutationContract> (HashSetOps.mutation) + +/// The creation contract for `HashSet`. +[] +type HashSetCreationTests () = + inherit SetCreationContract> (HashSetOps.creation) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOps.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOps.fs new file mode 100644 index 00000000..90268115 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOps.fs @@ -0,0 +1,110 @@ +/// The functions of the `HashSet` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.HashSetOps + +open System +open FSharp.Collections.Immutable + +let private toArrayPair (struct (first : HashSet<'T1>, second : HashSet<'T2>)) = + struct (HashSet.toArray first, HashSet.toArray second) + +/// Creates hash sets that compare by remainders: the struct of `forComparer` with `SetInputs.SameRemainder`. +let private ofSeqByRemainder (elements : seq) = (HashSet.forComparer SetInputs.SameRemainder).ofSeq elements + +/// The functions of the reading facet. +let reading : SetOps> = { + Name = nameof HashSet + ParamName = SetParamNames.ofFSharpCore + OfSeq = HashSet.ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = HashSet.toArray + + Count = HashSet.count + IsEmpty = HashSet.isEmpty + + TryGetValue = HashSet.tryGetValue + + Contains = HashSet.contains + Exists = HashSet.exists + Forall = HashSet.forall + IsProperSubset = HashSet.isProperSubset + IsProperSuperset = HashSet.isProperSuperset + IsSubset = HashSet.isSubset + IsSuperset = HashSet.isSuperset + Overlaps = HashSet.overlaps + SetEquals = HashSet.setEquals + + Fold = HashSet.fold + FoldBack = HashSet.foldBack + Iter = HashSet.iter + + ToList = HashSet.toList + ToSeq = HashSet.toSeq +} + +/// The functions of the transformation facet. +let transform : SetTransformOps> = { + Name = nameof HashSet + ParamName = SetParamNames.ofFSharpCore + OfSeq = HashSet.ofSeq + ToArray = HashSet.toArray + + Choose = fun chooser set -> HashSet.choose chooser set |> HashSet.toArray + Difference = HashSet.difference + Filter = HashSet.filter + Intersect = HashSet.intersect + IntersectMany = HashSet.intersectMany + Map = fun mapping set -> HashSet.map mapping set |> HashSet.toArray + SymmetricDifference = HashSet.symmetricDifference + Union = HashSet.union + UnionMany = HashSet.unionMany + + Partition = HashSet.partition + PartitionWith = fun partitioner set -> HashSet.partitionWith partitioner set |> toArrayPair +} + +/// The functions of the mutation facet. +let mutation : SetMutationOps> = { + Name = nameof HashSet + ParamName = SetParamNames.ofFSharpCore + OfSeq = HashSet.ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = HashSet.toArray + + Add = HashSet.add + AddRange = HashSet.addRange + Clear = HashSet.clear + Remove = HashSet.remove + RemoveRange = HashSet.removeRange +} + +/// The functions of the creation facet; the struct of `forComparer` compares with `SetInputs.SameRemainder`. +let creation : SetCreationOps> = + let custom = HashSet.forComparer SetInputs.SameRemainder + + { + Name = nameof HashSet + ParamName = SetParamNames.ofFSharpCore + ToArray = HashSet.toArray + Comparer = fun set -> HashSet.comparer set :> obj + + Empty = fun () -> HashSet.empty + OfArray = HashSet.ofArray + OfList = HashSet.ofList + OfReadOnlySpan = fun array -> HashSet.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = HashSet.ofSeq + OfSpan = fun array -> HashSet.ofSpan (Span array) + Singleton = HashSet.singleton + + CustomComparer = { + Comparer = custom.Comparer :> obj + + Empty = fun () -> custom.empty + OfArray = fun array -> custom.ofArray array + OfList = fun elements -> custom.ofList elements + OfReadOnlySpan = fun array -> custom.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = fun elements -> custom.ofSeq elements + OfSpan = fun array -> custom.ofSpan (Span array) + Singleton = fun value -> custom.singleton value + } + } diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOptionTests.fs new file mode 100644 index 00000000..59546a97 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetOptionTests.fs @@ -0,0 +1,61 @@ +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 `HashSet` module of the `Option` namespace. +[] +type HashSetOptionTests () = + + static let numbers = HashSet.ofArray Inputs.Several + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + [] + member _.``tryGetValue returns an option`` (value : int) = + let found : int option = HashSet.tryGetValue value numbers + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.HashSet.tryGetValue value numbers, + found, + "Option.HashSet.tryGetValue must match the default" + ) + + [] + member _.``choose takes a chooser of options`` () = + let chosen : HashSet = HashSet.choose negatedIfNegative numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.choose negatedIfNegative + |> Array.distinct + |> Array.sort, + HashSet.toArray chosen |> Array.sort, + "Option.HashSet.choose must keep the distinct values that Array.choose chooses" + ) + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let mapped : HashSet = HashSet.map (fun item -> item * 2) numbers + let (accepted : HashSet), (rejected : HashSet) = + HashSet.partition (fun item -> item > 0) numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.map (fun item -> item * 2) + |> Array.distinct + |> Array.sort, + HashSet.toArray mapped |> Array.sort, + "HashSet.map must resolve to the default module" + ) + + Assert.AreEqual( + HashSet.count numbers, + HashSet.count accepted + HashSet.count rejected, + "HashSet.partition must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTests.fs new file mode 100644 index 00000000..9e2afc0e --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTests.fs @@ -0,0 +1,771 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Globalization +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The behaviour of HashSet that the shared contracts do not cover: comparers, the struct of forComparer, +/// shared storage, null inputs and the extras. +/// +/// +/// The tables of functions are maps by name, and the names are the rows of their tests: MSTest names a row after its +/// values, and neither a function nor a hash set prints a readable value. +/// +[] +type HashSetTests () = + + static let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + static let defaultComparer : IEqualityComparer = EqualityComparer.Default + + /// Creates hash sets that compare without case. + static let caseless = HashSet.forComparer ignoreCase + + /// Words that compare without case, so that a value can equal an element that differs from it. + static let words () = caseless.ofArray [| "apple"; "Banana"; "cherry" |] + + /// The elements of a hash set, sorted in the order of F# generic comparison. + static let sorted (set : HashSet<'T>) = HashSet.toArray set |> Array.sort + + static let isLong (word : string) = word.Length > 5 + + static let byLength (word : string) = if isLong word then Choice1Of2 word else Choice2Of2 word + + static let nullSet = Unchecked.defaultof> + + static let nullComparer = Unchecked.defaultof> + + /// The hash set of a builder of the struct that added the elements. + static let ofCaselessBuilder (elements : string array) = + let builder = caseless.builder () + builder.UnionWith elements + HashSet.ofBuilder builder + + /// The creation functions without a comparer, by name. + static let defaultCreations : Map HashSet> = + Map.ofArray [| + nameof HashSet.empty, (fun () -> HashSet.empty) + nameof HashSet.ofArray, (fun () -> HashSet.ofArray [| "a" |]) + // The function under test takes a list + nameof HashSet.ofList, (fun () -> HashSet.ofList [ "a" ]) + nameof HashSet.ofReadOnlySpan, (fun () -> HashSet.ofReadOnlySpan (ReadOnlySpan [| "a" |])) + nameof HashSet.ofSeq, (fun () -> HashSet.ofSeq (Seq.singleton "a")) + nameof HashSet.ofSpan, (fun () -> HashSet.ofSpan (Span [| "a" |])) + nameof HashSet.singleton, (fun () -> HashSet.singleton "a") + nameof HashSet.build, (fun () -> HashSet.build (fun builder -> builder.Add "a" |> ignore)) + nameof HashSet.builder, (fun () -> HashSet.ofBuilder (HashSet.builder ())) + |] + + /// The members of the struct, by name, each with the hash set that it creates from "apple" and "APPLE". + static let structCreations : Map HashSet> = + Map.ofArray [| + nameof caseless.empty, (fun () -> caseless.empty |> HashSet.add "apple" |> HashSet.add "APPLE") + nameof caseless.ofArray, (fun () -> caseless.ofArray [| "apple"; "APPLE" |]) + // The member under test takes a list + nameof caseless.ofList, (fun () -> caseless.ofList [ "apple"; "APPLE" ]) + nameof caseless.ofReadOnlySpan, (fun () -> caseless.ofReadOnlySpan (ReadOnlySpan [| "apple"; "APPLE" |])) + nameof caseless.ofSeq, (fun () -> caseless.ofSeq (Seq.ofArray [| "apple"; "APPLE" |])) + nameof caseless.ofSpan, (fun () -> caseless.ofSpan (Span [| "apple"; "APPLE" |])) + nameof caseless.singleton, (fun () -> caseless.singleton "apple" |> HashSet.add "APPLE") + nameof caseless.build, (fun () -> caseless.build (fun builder -> builder.UnionWith [| "apple"; "APPLE" |])) + nameof caseless.builder, (fun () -> ofCaselessBuilder [| "apple"; "APPLE" |]) + |] + + /// The incremental changes, by name, applied to the words. + static let changes : Map -> HashSet> = + Map.ofArray [| + nameof HashSet.add, HashSet.add "date" + nameof HashSet.addRange, HashSet.addRange [| "APPLE"; "date" |] + nameof HashSet.clear, HashSet.clear + nameof HashSet.remove, HashSet.remove "APPLE" + nameof HashSet.removeRange, HashSet.removeRange [| "APPLE"; "date" |] + nameof HashSet.update, HashSet.update (fun builder -> builder.Add "date" |> ignore) + |] + + /// The functions that select elements, by name, applied to the words. + static let selections : Map -> HashSet> = + Map.ofArray [| + nameof HashSet.filter, HashSet.filter isLong + ($"%s{nameof HashSet.partition} (accepted)", + fun set -> + let struct (accepted, _) = HashSet.partition isLong set + accepted) + ($"%s{nameof HashSet.partition} (rejected)", + fun set -> + let struct (_, rejected) = HashSet.partition isLong set + rejected) + |] + + /// The set algebra, by name, with the elements that it returns for the words and the hash set of "APPLE" and "date". + static let algebra : Map -> HashSet -> HashSet) * string array)> = + Map.ofArray [| + nameof HashSet.difference, struct (HashSet.difference, [| "Banana"; "cherry" |]) + // Intersect keeps the elements of the second set + nameof HashSet.intersect, struct (HashSet.intersect, [| "APPLE" |]) + nameof HashSet.symmetricDifference, struct (HashSet.symmetricDifference, [| "Banana"; "cherry"; "date" |]) + nameof HashSet.union, struct (HashSet.union, [| "Banana"; "apple"; "cherry"; "date" |]) + |] + + /// The transformations to another element type, by name, applied to the words. + static let typeChanges : Map -> HashSet> = + Map.ofArray [| + nameof HashSet.map, HashSet.map id + nameof HashSet.choose, HashSet.choose ValueSome + ($"%s{nameof HashSet.partitionWith} (first)", + fun set -> + let struct (first, _) = HashSet.partitionWith byLength set + first) + ($"%s{nameof HashSet.partitionWith} (second)", + fun set -> + let struct (_, second) = HashSet.partitionWith byLength set + second) + |] + + /// The functions that take a hash set, by name and parameter, with the name of the parameter and a call with null. + static let nullRejections : Map unit))> = + let set = HashSet.ofArray Inputs.Several + + Map.ofArray [| + nameof HashSet.add, struct ("set", (fun () -> HashSet.add 1 nullSet |> ignore)) + nameof HashSet.addRange, struct ("set", (fun () -> HashSet.addRange [| 1 |] nullSet |> ignore)) + nameof HashSet.choose, struct ("set", (fun () -> HashSet.choose ValueSome nullSet |> ignore)) + nameof HashSet.clear, struct ("set", (fun () -> HashSet.clear nullSet |> ignore)) + nameof HashSet.comparer, struct ("set", (fun () -> HashSet.comparer nullSet |> ignore)) + nameof HashSet.contains, struct ("set", (fun () -> HashSet.contains 1 nullSet |> ignore)) + nameof HashSet.count, struct ("set", (fun () -> HashSet.count nullSet |> ignore)) + $"%s{nameof HashSet.difference} (set1)", struct ("set1", (fun () -> HashSet.difference nullSet set |> ignore)) + $"%s{nameof HashSet.difference} (set2)", struct ("set2", (fun () -> HashSet.difference set nullSet |> ignore)) + nameof HashSet.exists, struct ("set", (fun () -> HashSet.exists ((=) 1) nullSet |> ignore)) + nameof HashSet.filter, struct ("set", (fun () -> HashSet.filter ((=) 1) nullSet |> ignore)) + nameof HashSet.fold, struct ("set", (fun () -> HashSet.fold (+) 0 nullSet |> ignore)) + nameof HashSet.foldBack, struct ("set", (fun () -> HashSet.foldBack (+) nullSet 0 |> ignore)) + nameof HashSet.forall, struct ("set", (fun () -> HashSet.forall ((=) 1) nullSet |> ignore)) + $"%s{nameof HashSet.intersect} (set1)", struct ("set1", (fun () -> HashSet.intersect nullSet set |> ignore)) + $"%s{nameof HashSet.intersect} (set2)", struct ("set2", (fun () -> HashSet.intersect set nullSet |> ignore)) + nameof HashSet.intersectMany, struct ("sets", (fun () -> HashSet.intersectMany [| set; nullSet |] |> ignore)) + nameof HashSet.isEmpty, struct ("set", (fun () -> HashSet.isEmpty nullSet |> ignore)) + $"%s{nameof HashSet.isProperSubset} (set1)", struct ("set1", (fun () -> HashSet.isProperSubset nullSet set |> ignore)) + $"%s{nameof HashSet.isProperSubset} (set2)", struct ("set2", (fun () -> HashSet.isProperSubset set nullSet |> ignore)) + $"%s{nameof HashSet.isProperSuperset} (set1)", + struct ("set1", (fun () -> HashSet.isProperSuperset nullSet set |> ignore)) + $"%s{nameof HashSet.isProperSuperset} (set2)", + struct ("set2", (fun () -> HashSet.isProperSuperset set nullSet |> ignore)) + $"%s{nameof HashSet.isSubset} (set1)", struct ("set1", (fun () -> HashSet.isSubset nullSet set |> ignore)) + $"%s{nameof HashSet.isSubset} (set2)", struct ("set2", (fun () -> HashSet.isSubset set nullSet |> ignore)) + $"%s{nameof HashSet.isSuperset} (set1)", struct ("set1", (fun () -> HashSet.isSuperset nullSet set |> ignore)) + $"%s{nameof HashSet.isSuperset} (set2)", struct ("set2", (fun () -> HashSet.isSuperset set nullSet |> ignore)) + nameof HashSet.iter, struct ("set", (fun () -> HashSet.iter ignore nullSet)) + nameof HashSet.map, struct ("set", (fun () -> HashSet.map id nullSet |> ignore)) + $"%s{nameof HashSet.overlaps} (set1)", struct ("set1", (fun () -> HashSet.overlaps nullSet set |> ignore)) + $"%s{nameof HashSet.overlaps} (set2)", struct ("set2", (fun () -> HashSet.overlaps set nullSet |> ignore)) + nameof HashSet.partition, struct ("set", (fun () -> HashSet.partition ((=) 1) nullSet |> ignore)) + nameof HashSet.partitionWith, + struct ("set", + (fun () -> + HashSet.partitionWith (fun (item : int) -> Choice.Choice1Of2 item) nullSet + |> ignore + )) + nameof HashSet.remove, struct ("set", (fun () -> HashSet.remove 1 nullSet |> ignore)) + nameof HashSet.removeRange, struct ("set", (fun () -> HashSet.removeRange [| 1 |] nullSet |> ignore)) + $"%s{nameof HashSet.setEquals} (set1)", struct ("set1", (fun () -> HashSet.setEquals nullSet set |> ignore)) + $"%s{nameof HashSet.setEquals} (set2)", struct ("set2", (fun () -> HashSet.setEquals set nullSet |> ignore)) + $"%s{nameof HashSet.symmetricDifference} (set1)", + struct ("set1", (fun () -> HashSet.symmetricDifference nullSet set |> ignore)) + $"%s{nameof HashSet.symmetricDifference} (set2)", + struct ("set2", (fun () -> HashSet.symmetricDifference set nullSet |> ignore)) + nameof HashSet.toArray, struct ("set", (fun () -> HashSet.toArray nullSet |> ignore)) + nameof HashSet.toBuilder, struct ("set", (fun () -> HashSet.toBuilder nullSet |> ignore)) + nameof HashSet.toList, struct ("set", (fun () -> HashSet.toList nullSet |> ignore)) + nameof HashSet.toSeq, struct ("set", (fun () -> HashSet.toSeq nullSet |> ignore)) + nameof HashSet.tryGetValue, struct ("set", (fun () -> HashSet.tryGetValue 1 nullSet |> ignore)) + $"%s{nameof HashSet.union} (set1)", struct ("set1", (fun () -> HashSet.union nullSet set |> ignore)) + $"%s{nameof HashSet.union} (set2)", struct ("set2", (fun () -> HashSet.union set nullSet |> ignore)) + nameof HashSet.unionMany, struct ("sets", (fun () -> HashSet.unionMany [| nullSet; set |] |> ignore)) + nameof HashSet.update, struct ("set", (fun () -> HashSet.update ignore nullSet |> ignore)) + |] + + /// + /// 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 the sequence (ADR 0001, D6). + /// + static let sequenceRejections : Map unit))> = + let set = HashSet.ofArray Inputs.Several + let ints = HashSet.forComparer (EqualityComparer.Default :> IEqualityComparer) + let nullElements = Unchecked.defaultof> + let defaultElements = Unchecked.defaultof> :> seq + let nullSets = Unchecked.defaultof>> + let defaultSets = Unchecked.defaultof>> :> seq> + + // The flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widenedElements = FlatListOracles.defaultStringsAsObjects + let objects = HashSet.ofArray [| ("a" :> obj) |] + + Map.ofArray [| + $"%s{nameof HashSet.addRange} (null elements)", + struct ("elements", (fun () -> HashSet.addRange nullElements set |> ignore)) + $"%s{nameof HashSet.addRange} (default flat list)", + struct ("elements", (fun () -> HashSet.addRange defaultElements set |> ignore)) + $"%s{nameof HashSet.addRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> HashSet.addRange widenedElements objects |> ignore)) + $"%s{nameof HashSet.intersectMany} (null sets)", struct ("sets", (fun () -> HashSet.intersectMany nullSets |> ignore)) + $"%s{nameof HashSet.intersectMany} (default flat list)", + struct ("sets", (fun () -> HashSet.intersectMany defaultSets |> ignore)) + $"%s{nameof HashSet.ofSeq} (null elements)", struct ("elements", (fun () -> HashSet.ofSeq nullElements |> ignore)) + $"%s{nameof HashSet.ofSeq} (default flat list)", + struct ("elements", (fun () -> HashSet.ofSeq defaultElements |> ignore)) + $"%s{nameof HashSet.ofSeq} (default flat list of strings as objects)", + struct ("elements", (fun () -> HashSet.ofSeq widenedElements |> ignore)) + $"%s{nameof HashSet.removeRange} (null elements)", + struct ("elements", (fun () -> HashSet.removeRange nullElements set |> ignore)) + $"%s{nameof HashSet.removeRange} (default flat list)", + struct ("elements", (fun () -> HashSet.removeRange defaultElements set |> ignore)) + $"%s{nameof HashSet.removeRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> HashSet.removeRange widenedElements objects |> ignore)) + $"%s{nameof HashSet.unionMany} (null sets)", struct ("sets", (fun () -> HashSet.unionMany nullSets |> ignore)) + $"%s{nameof HashSet.unionMany} (default flat list)", + struct ("sets", (fun () -> HashSet.unionMany defaultSets |> ignore)) + // The struct of forComparer + $"forComparer %s{nameof ints.ofSeq} (null elements)", + struct ("elements", (fun () -> ints.ofSeq nullElements |> ignore)) + $"forComparer %s{nameof ints.ofSeq} (default flat list)", + struct ("elements", (fun () -> ints.ofSeq defaultElements |> ignore)) + |] + + /// + /// The relations of a hash set with a sequence, by name: the member of the BCL type, which takes the sequence, and the + /// function of the module, which takes a hash set. + /// + static let relationFunctions + : Map -> seq -> bool) * (HashSet -> HashSet -> bool))> = + Map.ofArray [| + nameof HashSet.isProperSubset, struct ((fun set elements -> set.IsProperSubsetOf elements), HashSet.isProperSubset) + nameof HashSet.isProperSuperset, + struct ((fun set elements -> set.IsProperSupersetOf elements), HashSet.isProperSuperset) + nameof HashSet.isSubset, struct ((fun set elements -> set.IsSubsetOf elements), HashSet.isSubset) + nameof HashSet.isSuperset, struct ((fun set elements -> set.IsSupersetOf elements), HashSet.isSuperset) + nameof HashSet.overlaps, struct ((fun set elements -> set.Overlaps elements), HashSet.overlaps) + nameof HashSet.setEquals, struct ((fun set elements -> set.SetEquals elements), HashSet.setEquals) + |] + + /// + /// The operations of the set algebra with a sequence, by name: the member of the BCL type, which takes the sequence, and + /// the function of the module, which takes a hash set. + /// + static let operationFunctions + : Map< + string, + struct ((HashSet -> seq -> HashSet) * + (HashSet -> HashSet -> HashSet)) + > = + Map.ofArray [| + nameof HashSet.intersect, struct ((fun set elements -> set.Intersect elements), HashSet.intersect) + nameof HashSet.symmetricDifference, + struct ((fun set elements -> set.SymmetricExcept elements), HashSet.symmetricDifference) + |] + + /// The relations that need no conversion, by name, as the changelog gives them: each tests the elements with contains. + static let streamingRecipes : Map -> seq -> bool> = + Map.ofArray [| + nameof HashSet.isSuperset, (fun set elements -> Seq.forall (fun item -> HashSet.contains item set) elements) + nameof HashSet.overlaps, (fun set elements -> Seq.exists (fun item -> HashSet.contains item set) elements) + |] + + /// The names of the creation functions without a comparer, as rows. + static member DefaultCreations = defaultCreations.Keys + + /// The names of the members of the struct that create hash sets, as rows. + static member StructCreations = structCreations.Keys + + /// The names of the incremental changes, as rows. + static member Changes = changes.Keys + + /// The names of the functions that select elements, as rows. + static member Selections = selections.Keys + + /// The names of the set algebra, as rows. + static member Algebra = algebra.Keys + + /// The names of the transformations to another element type, as rows. + static member TypeChanges = typeChanges.Keys + + /// The names of the functions that take a hash set, with the parameter where a function takes two, as rows. + static member NullRejections = nullRejections.Keys + + /// The names of the functions that take a sequence, with the input that they reject, as rows. + static member SequenceRejections = sequenceRejections.Keys + + // ---- Comparers ---- + + [] + member _.``creation without a comparer compares with the default comparer`` (name : string) = + Assert.AreSame ( + defaultComparer, + HashSet.comparer (defaultCreations[name]()), + $"HashSet.%s{name} must compare with EqualityComparer.Default" + ) + + [] + member _.``forComparer creates the struct with the comparer`` () = + Assert.AreSame (ignoreCase, (HashSet.forComparer ignoreCase).Comparer, "HashSet.forComparer must keep the comparer") + + Assert.AreSame ( + ignoreCase, + (CustomComparerHashSet ignoreCase).Comparer, + "The constructor of CustomComparerHashSet must keep the comparer" + ) + + [] + member _.``the members of the struct create hash sets that compare with its comparer`` (name : string) = + let set = structCreations[name]() + Assert.AreSame (ignoreCase, HashSet.comparer set, $"CustomComparerHashSet.%s{name} must keep the comparer") + + Assert.AreEqual( + "apple", + Assert.ContainsSingle (set, $"CustomComparerHashSet.%s{name} must compare without case"), + $"CustomComparerHashSet.%s{name} must keep the first of the elements that the comparer considers equal" + ) + + [] + member _.``the struct gives an existing hash set its comparer`` () = + let set = HashSet.ofArray [| "apple"; "APPLE"; "Banana" |] + let ignoringCase = caseless.ofSeq set + + Assert.AreSame ( + ignoreCase, + HashSet.comparer ignoringCase, + "CustomComparerHashSet.ofSeq must give the hash set the comparer" + ) + Assert.HasCount (2, ignoringCase, "CustomComparerHashSet.ofSeq must merge the elements that the comparer considers equal") + Assert.HasCount (3, set, "CustomComparerHashSet.ofSeq must not change the hash set") + + Assert.AreSame ( + ignoringCase, + caseless.ofSeq ignoringCase, + "CustomComparerHashSet.ofSeq of a hash set with the comparer must return it" + ) + + // The BCL tests the instance of the comparer, not its behaviour, so two equal comparers are two comparers (D14) + [] + member _.``the struct returns a hash set only for the same comparer instance`` () = + let first : IEqualityComparer = StringComparer.Create (CultureInfo.InvariantCulture, true) + let second : IEqualityComparer = StringComparer.Create (CultureInfo.InvariantCulture, true) + Assert.AreNotSame (first, second, "The test needs two comparer instances") + Assert.AreEqual(first, second, "The test needs two comparers that are equal") + + let set = (HashSet.forComparer first).ofArray [| "apple"; "Banana" |] + + Assert.AreSame ( + set, + (HashSet.forComparer first).ofSeq set, + "CustomComparerHashSet.ofSeq must return a hash set that has the same comparer instance" + ) + + let rebuilt = (HashSet.forComparer second).ofSeq set + Assert.AreNotSame (set, rebuilt, "CustomComparerHashSet.ofSeq must rebuild a hash set whose comparer is another instance") + Assert.AreSame ( + second, + HashSet.comparer rebuilt, + "CustomComparerHashSet.ofSeq must give the rebuilt hash set its comparer" + ) + CollectionAssert.AreEqual ([| "Banana"; "apple" |], sorted rebuilt, "CustomComparerHashSet.ofSeq must keep the elements") + + [] + member _.``the default value of the struct compares with the default comparer`` () = + let defaultValue = Unchecked.defaultof> + let set = defaultValue.ofArray [| "apple"; "APPLE" |] + + Assert.AreSame ( + defaultComparer, + defaultValue.Comparer, + "The default CustomComparerHashSet must return the default comparer" + ) + Assert.AreSame (defaultComparer, HashSet.comparer set, "The default CustomComparerHashSet must create hash sets with it") + Assert.HasCount (2, set, "The default CustomComparerHashSet must tell the case apart") + + [] + member _.``add, addRange, clear, remove, removeRange and update keep the comparer`` (name : string) = + Assert.AreSame (ignoreCase, HashSet.comparer (changes[name](words ())), $"HashSet.%s{name} must keep the comparer") + + [] + member _.``addRange adds the elements that are not equal in the comparer to an element of the hash set`` () = + // "APPLE" equals "apple" without case, so the hash set keeps "apple" (ADR 0001, D14) + CollectionAssert.AreEqual ( + [| "Banana"; "apple"; "cherry"; "date" |], + sorted (HashSet.addRange [| "APPLE"; "date" |] (words ())), + "HashSet.addRange must compare with the comparer of the hash set" + ) + + [] + member _.``addRange to an empty hash set keeps its comparer for a hash set with another comparer`` () = + // ImmutableHashSet.Union answers an empty hash set with the hash set of its argument, rebuilt with the comparer + let result = HashSet.addRange (HashSet.ofArray [| "apple"; "APPLE" |]) caseless.empty + + Assert.AreSame (ignoreCase, HashSet.comparer result, "HashSet.addRange must keep the comparer of the hash set") + Assert.HasCount (1, result, "HashSet.addRange must compare with the comparer of the hash set") + + // ImmutableHashSet.Union answers an empty input that receives a hash set with that hash set, or with a copy of it that + // has the comparer of the input, so the empty input itself never comes back from the BCL member (ADR 0001, D14) + [] + member _.``addRange returns an empty input for an empty hash set`` () = + let set = caseless.empty + + Assert.AreSame ( + set, + HashSet.addRange HashSet.empty set, + "HashSet.addRange of an empty hash set with another comparer must return the input" + ) + + Assert.AreSame ( + set, + HashSet.addRange caseless.empty set, + "HashSet.addRange of an empty hash set with the same comparer must return the input" + ) + + [] + member _.``remove removes the element that equals the value in the comparer`` () = + CollectionAssert.AreEqual ( + [| "Banana"; "cherry" |], + sorted (HashSet.remove "APPLE" (words ())), + "HashSet.remove must remove the element that equals the value in the comparer" + ) + + [] + member _.``removeRange removes the elements that are equal in the comparer to an element of the sequence`` () = + CollectionAssert.AreEqual ( + [| "Banana"; "cherry" |], + sorted (HashSet.removeRange [| "APPLE"; "date" |] (words ())), + "HashSet.removeRange must compare with the comparer of the hash set" + ) + + [] + member _.``filter and partition keep the comparer`` (name : string) = + Assert.AreSame (ignoreCase, HashSet.comparer (selections[name](words ())), $"HashSet.%s{name} must keep the comparer") + + [] + member _.``partition keeps comparing without case`` () = + let struct (accepted, _) = HashSet.partition isLong (words ()) + Assert.IsTrue (HashSet.contains "BANANA" accepted, "HashSet.partition must keep comparing without case") + + [] + member _.``set algebra keeps the comparer of the first set and compares with it`` (name : string) = + let struct (combine, expected) = algebra[name] + let result = combine (words ()) (HashSet.ofArray [| "APPLE"; "date" |]) + + Assert.AreSame (ignoreCase, HashSet.comparer result, $"HashSet.%s{name} must keep the comparer of the first set") + CollectionAssert.AreEqual (expected, sorted result, $"HashSet.%s{name} must compare with the comparer of the first set") + + [] + member _.``isProperSuperset counts the elements that the comparer of the first set finds equal once`` () = + // The second set tells "apple" and "APPLE" apart; for the first set they are one element, and it holds "Banana" + // besides, as the mutable HashSet answers (ADR 0001, D14) + Assert.IsTrue ( + HashSet.isProperSuperset (caseless.ofArray [| "apple"; "Banana" |]) (HashSet.ofArray [| "apple"; "APPLE" |]), + "HashSet.isProperSuperset must count the elements that the comparer of the first set finds equal once" + ) + + [] + member _.``the predicates of two sets compare with the comparer of the first set`` () = + let apple = caseless.ofArray [| "APPLE" |] + let set = words () + + Assert.IsTrue (HashSet.isSubset apple set, "HashSet.isSubset must compare without case") + Assert.IsTrue (HashSet.isProperSubset apple set, "HashSet.isProperSubset must compare without case") + Assert.IsTrue (HashSet.isSuperset set apple, "HashSet.isSuperset must compare without case") + Assert.IsTrue (HashSet.isProperSuperset set apple, "HashSet.isProperSuperset must compare without case") + Assert.IsTrue (HashSet.overlaps apple set, "HashSet.overlaps must compare without case") + + Assert.IsTrue ( + HashSet.setEquals set (HashSet.ofArray [| "APPLE"; "BANANA"; "CHERRY" |]), + "HashSet.setEquals must compare without case" + ) + + Assert.IsFalse ( + HashSet.isSubset (HashSet.ofArray [| "APPLE" |]) set, + "HashSet.isSubset must compare with the default comparer of the first set" + ) + + [] + member _.``intersectMany and unionMany keep the comparer of the first set`` () = + let sets = [| words (); HashSet.ofArray [| "APPLE"; "date" |] |] + let intersection = HashSet.intersectMany sets + let union = HashSet.unionMany sets + + Assert.AreSame ( + ignoreCase, + HashSet.comparer intersection, + "HashSet.intersectMany must keep the comparer of the first set" + ) + Assert.AreSame (ignoreCase, HashSet.comparer union, "HashSet.unionMany must keep the comparer of the first set") + CollectionAssert.AreEqual ([| "APPLE" |], sorted intersection, "HashSet.intersectMany must compare without case") + + CollectionAssert.AreEqual ( + [| "Banana"; "apple"; "cherry"; "date" |], + sorted union, + "HashSet.unionMany must compare without case" + ) + + [] + member _.``unionMany of no sets is empty with the default comparer`` () = + let union = HashSet.unionMany Seq.empty> + Assert.IsEmpty (union, "HashSet.unionMany of no sets must be empty") + Assert.AreSame ( + defaultComparer, + HashSet.comparer union, + "HashSet.unionMany of no sets must compare with the default comparer" + ) + + [] + member _.``map, choose and partitionWith compare with the default comparer of the result`` (name : string) = + Assert.AreSame ( + defaultComparer, + HashSet.comparer (typeChanges[name](words ())), + $"HashSet.%s{name} must compare with the default comparer" + ) + + [] + member _.``map does not keep the comparer of its input`` () = + // The default comparer tells the case apart, although the element type is the same + Assert.HasCount ( + 4, + HashSet.add "APPLE" (HashSet.map id (words ())), + "HashSet.map must not keep the comparer of its input" + ) + + // ---- Sharing ---- + + [] + member _.``ofSeq of a hash set with the default comparer returns it`` () = + let set = HashSet.ofArray Inputs.Several + Assert.AreSame (set, HashSet.ofSeq set, "HashSet.ofSeq of a hash set with the default comparer must not copy it") + + let copy = HashSet.ofSeq (words ()) + Assert.AreSame (defaultComparer, HashSet.comparer copy, "HashSet.ofSeq must compare with the default comparer") + Assert.HasCount (3, copy, "HashSet.ofSeq must keep the elements of a hash set with another comparer") + + [] + member _.``add returns the input when it contains an equal element`` () = + let set = words () + Assert.AreSame (set, HashSet.add "APPLE" set, "HashSet.add of an element equal in the comparer must return the input") + + [] + member _.``remove returns the input when it has no equal element`` () = + let set = words () + Assert.AreSame (set, HashSet.remove "date" set, "HashSet.remove of a missing element must return the input") + + [] + member _.``clear returns an empty input`` () = + let set = caseless.empty + Assert.AreSame (set, HashSet.clear set, "HashSet.clear of an empty hash set must return the input") + + // ---- Access and search ---- + + [] + member _.``tryGetValue returns the element that the hash set holds`` () = + let stored = String ('x', 3) + let set = caseless.ofArray [| stored; "apple" |] + + match HashSet.tryGetValue "XXX" set with + | ValueSome found -> Assert.AreSame (stored, found, "HashSet.tryGetValue must return the instance of the hash set") + | ValueNone -> Assert.Fail "HashSet.tryGetValue must find the element that equals the value in the comparer" + + Assert.AreEqual( + ValueSome "apple", + HashSet.tryGetValue "APPLE" set, + "HashSet.tryGetValue must compare without case" + ) + Assert.AreEqual( + ValueNone, + HashSet.tryGetValue "date" set, + "HashSet.tryGetValue must return nothing for a missing value" + ) + + [] + member _.``contains compares with the comparer of the hash set`` () = + Assert.IsTrue (HashSet.contains "BANANA" (words ()), "HashSet.contains must compare without case") + Assert.IsFalse ( + HashSet.contains "BANANA" (HashSet.ofArray [| "Banana" |]), + "HashSet.contains must compare with the default comparer" + ) + + // ---- Relations with a sequence ---- + + // The way to pass a sequence to a relation, as the changelog gives it: convert the sequence with the comparer of the + // first set. The default comparer gives the same answer when the comparer of the first set finds equal what the default + // comparer finds equal, and another answer when it tells such strings apart, as the reference comparer does + [)>] + member _.``a relation with a sequence takes the sequence that the comparer of the first set converts`` + (relation : string, comparer : string) + = + let instances = SetRelationInputs.Strings [| "a1"; "a2" |] + Assert.AreNotSame ( + instances[0], + instances[1], + "The test needs two strings with the same text that are different instances" + ) + + let struct (setItems, sequenceItems) = SetRelationInputs.Items[relation] + let struct (bclMember, moduleFunction) = relationFunctions[relation] + let struct (firstComparer, defaultFindsEqualWhatItFindsEqual) = SetRelationInputs.Comparers[comparer] + let set = (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) + let elements = SetRelationInputs.Strings sequenceItems + let expected = bclMember set elements + + Assert.AreEqual( + expected, + moduleFunction set ((HashSet.forComparer (HashSet.comparer set)).ofSeq elements), + $"HashSet.%s{relation} must answer as the member of the BCL type for the sequence that the comparer of the first set converts" + ) + + let withDefaultComparer = moduleFunction set (HashSet.ofSeq elements) + + if defaultFindsEqualWhatItFindsEqual then + Assert.AreEqual( + expected, + withDefaultComparer, + $"HashSet.%s{relation} must answer as the member of the BCL type for the sequence that the default comparer converts, because the comparer of the first set finds equal what the default comparer finds equal" + ) + else + Assert.AreNotEqual( + expected, + withDefaultComparer, + $"HashSet.%s{relation} must answer differently for the sequence that the default comparer converts, because the comparer of the first set tells apart strings that the default comparer finds equal" + ) + + // The same recipe for the operations of the set algebra that take a sequence in the BCL type + [)>] + member _.``an operation with a sequence takes the sequence that the comparer of the first set converts`` + (operation : string, comparer : string) + = + let struct (setItems, sequenceItems) = SetRelationInputs.OperationItems[operation] + let struct (bclMember, moduleFunction) = operationFunctions[operation] + let struct (firstComparer, defaultFindsEqualWhatItFindsEqual) = SetRelationInputs.Comparers[comparer] + let set = (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) + let elements = SetRelationInputs.Strings sequenceItems + let expected = HashSet.toArray (bclMember set elements) + + CollectionAssert.AreEquivalent ( + expected, + HashSet.toArray (moduleFunction set ((HashSet.forComparer (HashSet.comparer set)).ofSeq elements)), + $"HashSet.%s{operation} must return the result of the member of the BCL type for the sequence that the comparer of the first set converts" + ) + + let withDefaultComparer = HashSet.toArray (moduleFunction set (HashSet.ofSeq elements)) + + if defaultFindsEqualWhatItFindsEqual then + CollectionAssert.AreEquivalent ( + expected, + withDefaultComparer, + $"HashSet.%s{operation} must return the result of the member of the BCL type for the sequence that the default comparer converts, because the comparer of the first set finds equal what the default comparer finds equal" + ) + else + CollectionAssert.AreNotEquivalent ( + expected, + withDefaultComparer, + $"HashSet.%s{operation} must return another result for the sequence that the default comparer converts, because the comparer of the first set tells apart strings that the default comparer finds equal" + ) + + [)>] + member _.``overlaps and isSuperset with a sequence test each element with contains`` (relation : string, comparer : string) = + let struct (setItems, sequenceItems) = SetRelationInputs.Items[relation] + let struct (bclMember, _) = relationFunctions[relation] + let struct (firstComparer, _) = SetRelationInputs.Comparers[comparer] + let set = (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) + let elements = SetRelationInputs.Strings sequenceItems + + Assert.AreEqual( + bclMember set elements, + streamingRecipes[relation] set elements, + $"%s{relation} with a sequence must test each element with HashSet.contains, in the comparer of the first set" + ) + + // ---- Null inputs ---- + + [] + member _.``functions reject a null hash set`` (name : string) = + let struct (paramName, actual) = nullRejections[name] + + let ex = + Assert.ThrowsExactly(Action actual, $"HashSet.%s{name} must reject a null hash set") + + Assert.AreEqual(paramName, ex.ParamName, $"HashSet.%s{name} must name the parameter of the hash set") + + [] + member _.``functions that take a sequence reject a null sequence and a default flat list given as the sequence`` + (name : string) + = + let struct (paramName, actual) = sequenceRejections[name] + FlatListAssert.RejectsNull ($"HashSet.%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 objects = HashSet.ofArray [| ("b" :> obj); ("c" :> obj) |] + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + HashSet.toArray (HashSet.ofSeq widened), + "HashSet.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj) |], + HashSet.toArray ((HashSet.forComparer (EqualityComparer.Default :> IEqualityComparer)).ofSeq widened), + "CustomComparerHashSet.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj); ("c" :> obj) |], + HashSet.toArray (HashSet.addRange widened objects), + "HashSet.addRange must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| ("c" :> obj) |], + HashSet.toArray (HashSet.removeRange widened objects), + "HashSet.removeRange must take the elements of a flat list of strings as objects" + ) + + [] + member _.``ofBuilder rejects a null builder`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + HashSet.ofBuilder (Unchecked.defaultof.Builder>) + |> ignore + ), + "HashSet.ofBuilder must reject a null builder" + ) + + Assert.AreEqual("builder", ex.ParamName, "HashSet.ofBuilder must name the parameter builder") + + // forComparer calls the constructor of the struct, which checks the comparer, so both name its parameter (D12) + [] + member _.``forComparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> HashSet.forComparer nullComparer |> ignore), + "HashSet.forComparer must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "HashSet.forComparer must name the parameter comparer") + + [] + member _.``the constructor of the struct rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> CustomComparerHashSet nullComparer |> ignore), + "The constructor of CustomComparerHashSet must reject a null comparer" + ) + + Assert.AreEqual( + "comparer", + ex.ParamName, + "The constructor of CustomComparerHashSet must name the parameter comparer" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTupleTests.fs new file mode 100644 index 00000000..1c2201b6 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/HashSetTupleTests.fs @@ -0,0 +1,61 @@ +namespace FSharp.Collections.Immutable.Tests + +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 `HashSet` module of the `Tuple` namespace. +[] +type HashSetTupleTests () = + + static let numbers = HashSet.ofArray Inputs.Several + + static let expected = Set.ofArray Inputs.Several + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : HashSet), (rejected : HashSet) = + HashSet.partition (fun item -> item > 0) numbers + let expectedAccepted, expectedRejected = Set.partition (fun item -> item > 0) expected + + CollectionAssert.AreEqual ( + Set.toArray expectedAccepted, + HashSet.toArray accepted |> Array.sort, + "Tuple.HashSet.partition must return the accepted elements" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedRejected, + HashSet.toArray rejected |> Array.sort, + "Tuple.HashSet.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 : HashSet), (second : HashSet) = HashSet.partitionWith partitioner numbers + let expectedFirst, expectedSecond = Set.partitionWith partitioner expected + + CollectionAssert.AreEqual ( + Set.toArray expectedFirst, + HashSet.toArray first |> Array.sort, + "Tuple.HashSet.partitionWith must return the first choices" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedSecond, + HashSet.toArray second |> Array.sort, + "Tuple.HashSet.partitionWith must return the second choices" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = HashSet.tryGetValue 8 numbers + Assert.AreEqual(ValueSome 8, found, "HashSet.tryGetValue must resolve to the default module") diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetContractTests.fs new file mode 100644 index 00000000..7780f6b0 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetContractTests.fs @@ -0,0 +1,21 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The interface module has no creation functions, so it has no creation contract (ADR 0001, Appendix C). + +/// The reading contract for `ISet`, over hash sets behind the interface. +[] +type ISetSetTests () = + inherit SetContract> (ISetOps.reading) + +/// The transformation contract for `ISet`, over hash sets behind the interface. +[] +type ISetTransformTests () = + inherit SetTransformContract> (ISetOps.transform) + +/// The mutation contract for `ISet`, over hash sets behind the interface. +[] +type ISetMutationTests () = + inherit SetMutationContract> (ISetOps.mutation) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOps.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOps.fs new file mode 100644 index 00000000..45ae32d0 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOps.fs @@ -0,0 +1,83 @@ +/// The functions of the `ISet` module for the contract tests, over hash sets behind the interface. +[] +module FSharp.Collections.Immutable.Tests.ISetOps + +open System.Collections.Immutable +open FSharp.Collections.Immutable + +/// The sets of the contract tests are hash sets behind the interface. +let private ofSeq (elements : seq) = HashSet.ofSeq elements :> IImmutableSet + +/// The sets that compare by remainders are hash sets of the struct of `forComparer` with `SetInputs.SameRemainder`. +let private ofSeqByRemainder (elements : seq) = + (HashSet.forComparer SetInputs.SameRemainder).ofSeq elements :> IImmutableSet + +let private toArrayPair (struct (first : HashSet<'T1>, second : HashSet<'T2>)) = + struct (HashSet.toArray first, HashSet.toArray second) + +/// The functions of the reading facet. +let reading : SetOps> = { + Name = nameof ISet + ParamName = SetParamNames.ofFSharpCore + OfSeq = ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = ISet.toArray + + Count = ISet.count + IsEmpty = ISet.isEmpty + + TryGetValue = ISet.tryGetValue + + Contains = ISet.contains + Exists = ISet.exists + Forall = ISet.forall + IsProperSubset = ISet.isProperSubset + IsProperSuperset = ISet.isProperSuperset + IsSubset = ISet.isSubset + IsSuperset = ISet.isSuperset + Overlaps = ISet.overlaps + SetEquals = ISet.setEquals + + Fold = ISet.fold + FoldBack = ISet.foldBack + Iter = ISet.iter + + ToList = ISet.toList + ToSeq = ISet.toSeq +} + +/// The functions of the transformation facet; the functions to another element type return hash sets. +let transform : SetTransformOps> = { + Name = nameof ISet + ParamName = SetParamNames.ofFSharpCore + OfSeq = ofSeq + ToArray = ISet.toArray + + Choose = fun chooser set -> ISet.choose chooser set |> HashSet.toArray + Difference = ISet.difference + Filter = ISet.filter + Intersect = ISet.intersect + IntersectMany = ISet.intersectMany + Map = fun mapping set -> ISet.map mapping set |> HashSet.toArray + SymmetricDifference = ISet.symmetricDifference + Union = ISet.union + UnionMany = ISet.unionMany + + Partition = ISet.partition + PartitionWith = fun partitioner set -> ISet.partitionWith partitioner set |> toArrayPair +} + +/// The functions of the mutation facet. +let mutation : SetMutationOps> = { + Name = nameof ISet + ParamName = SetParamNames.ofFSharpCore + OfSeq = ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = ISet.toArray + + Add = ISet.add + AddRange = ISet.addRange + Clear = ISet.clear + Remove = ISet.remove + RemoveRange = ISet.removeRange +} diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOptionTests.fs new file mode 100644 index 00000000..a49cc2b5 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetOptionTests.fs @@ -0,0 +1,62 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Immutable +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 `ISet` module of the `Option` namespace. +[] +type ISetOptionTests () = + + static let numbers = HashSet.ofArray Inputs.Several :> IImmutableSet + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + [] + member _.``tryGetValue returns an option`` (value : int) = + let found : int option = ISet.tryGetValue value numbers + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.ISet.tryGetValue value numbers, + found, + "Option.ISet.tryGetValue must match the default" + ) + + [] + member _.``choose takes a chooser of options`` () = + let chosen : HashSet = ISet.choose negatedIfNegative numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.choose negatedIfNegative + |> Array.distinct + |> Array.sort, + HashSet.toArray chosen |> Array.sort, + "Option.ISet.choose must keep the distinct values that Array.choose chooses" + ) + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let filtered : IImmutableSet = ISet.filter (fun item -> item > 0) numbers + let (accepted : IImmutableSet), (rejected : IImmutableSet) = + ISet.partition (fun item -> item > 0) numbers + + CollectionAssert.AreEqual ( + Inputs.Several + |> Array.filter (fun item -> item > 0) + |> Array.distinct + |> Array.sort, + ISet.toArray filtered |> Array.sort, + "ISet.filter must resolve to the default module" + ) + + Assert.AreEqual( + ISet.count numbers, + ISet.count accepted + ISet.count rejected, + "ISet.partition must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs new file mode 100644 index 00000000..ec4e457b --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs @@ -0,0 +1,520 @@ +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 behaviour of ISet that the shared contracts do not cover: the implementation and the comparer of the input, +/// the hash sets of the transformations to another element type and null inputs. +/// +/// +/// The tables of functions are maps by name, and the names are the rows of their tests: MSTest names a row after its +/// values, and a function prints no readable value. +/// +[] +type ISetTests () = + + static let ignoreCase : IEqualityComparer = StringComparer.OrdinalIgnoreCase + + static let defaultComparer : IEqualityComparer = EqualityComparer.Default + + /// A sorted set behind the interface, whose implementation differs from the hash sets of the module. + static let sortedSet (elements : int array) = ImmutableSortedSet.CreateRange elements :> IImmutableSet + + /// Words that compare without case, in a hash set behind the interface. + static let words () = + (HashSet.forComparer ignoreCase).ofArray [| "apple"; "Banana"; "cherry" |] :> IImmutableSet + + /// The comparer of a hash set behind the interface. + static let comparerOf (set : IImmutableSet) (message : string) = + let hashSet = Assert.IsInstanceOfType>(set, message) + hashSet.KeyComparer + + static let isLong (word : string) = word.Length > 5 + + static let byLength (word : string) = if isLong word then Choice1Of2 word else Choice2Of2 word + + static let nullSet = Unchecked.defaultof> + + /// The transformations that keep the element type, by name, applied to a set and another set. + static let sameTypeTransformations : Map -> IImmutableSet -> IImmutableSet> = + Map.ofArray [| + nameof ISet.add, (fun set _ -> ISet.add 100 set) + nameof ISet.addRange, (fun set other -> ISet.addRange other set) + nameof ISet.clear, (fun set _ -> ISet.clear set) + nameof ISet.difference, ISet.difference + nameof ISet.filter, (fun set _ -> ISet.filter (fun item -> item > 0) set) + nameof ISet.intersect, ISet.intersect + nameof ISet.intersectMany, (fun set other -> ISet.intersectMany [| set; other |]) + ($"%s{nameof ISet.partition} (accepted)", + fun set _ -> + let struct (accepted, _) = ISet.partition (fun item -> item > 0) set + accepted) + ($"%s{nameof ISet.partition} (rejected)", + fun set _ -> + let struct (_, rejected) = ISet.partition (fun item -> item > 0) set + rejected) + nameof ISet.remove, (fun set _ -> ISet.remove 5 set) + nameof ISet.removeRange, (fun set other -> ISet.removeRange other set) + nameof ISet.symmetricDifference, ISet.symmetricDifference + nameof ISet.union, ISet.union + nameof ISet.unionMany, (fun set other -> ISet.unionMany [| set; other |]) + |] + + /// The transformations that keep the comparer, by name, applied to the words. + static let comparerKeepers : Map -> IImmutableSet> = + Map.ofArray [| + nameof ISet.add, ISet.add "date" + nameof ISet.addRange, ISet.addRange [| "APPLE"; "date" |] + nameof ISet.clear, ISet.clear + nameof ISet.filter, ISet.filter isLong + ($"%s{nameof ISet.partition} (accepted)", + fun set -> + let struct (accepted, _) = ISet.partition isLong set + accepted) + ($"%s{nameof ISet.partition} (rejected)", + fun set -> + let struct (_, rejected) = ISet.partition isLong set + rejected) + nameof ISet.remove, ISet.remove "APPLE" + nameof ISet.removeRange, ISet.removeRange [| "APPLE"; "date" |] + nameof ISet.union, (fun set -> ISet.union set (HashSet.ofArray [| "APPLE"; "date" |])) + |] + + /// The transformations to another element type, by name, applied to the words. + static let typeChanges : Map -> HashSet> = + Map.ofArray [| + nameof ISet.map, ISet.map id + nameof ISet.choose, ISet.choose ValueSome + ($"%s{nameof ISet.partitionWith} (first)", + fun set -> + let struct (first, _) = ISet.partitionWith byLength set + first) + ($"%s{nameof ISet.partitionWith} (second)", + fun set -> + let struct (_, second) = ISet.partitionWith byLength set + second) + |] + + /// The functions that take a set, by name and parameter, with the name of the parameter and a call with null. + static let nullRejections : Map unit))> = + let set = HashSet.ofArray Inputs.Several :> IImmutableSet + + Map.ofArray [| + nameof ISet.add, struct ("set", (fun () -> ISet.add 1 nullSet |> ignore)) + nameof ISet.addRange, struct ("set", (fun () -> ISet.addRange [| 1 |] nullSet |> ignore)) + nameof ISet.choose, struct ("set", (fun () -> ISet.choose ValueSome nullSet |> ignore)) + nameof ISet.clear, struct ("set", (fun () -> ISet.clear nullSet |> ignore)) + nameof ISet.contains, struct ("set", (fun () -> ISet.contains 1 nullSet |> ignore)) + nameof ISet.count, struct ("set", (fun () -> ISet.count nullSet |> ignore)) + $"%s{nameof ISet.difference} (set1)", struct ("set1", (fun () -> ISet.difference nullSet set |> ignore)) + $"%s{nameof ISet.difference} (set2)", struct ("set2", (fun () -> ISet.difference set nullSet |> ignore)) + nameof ISet.exists, struct ("set", (fun () -> ISet.exists ((=) 1) nullSet |> ignore)) + nameof ISet.filter, struct ("set", (fun () -> ISet.filter ((=) 1) nullSet |> ignore)) + nameof ISet.fold, struct ("set", (fun () -> ISet.fold (+) 0 nullSet |> ignore)) + nameof ISet.foldBack, struct ("set", (fun () -> ISet.foldBack (+) nullSet 0 |> ignore)) + nameof ISet.forall, struct ("set", (fun () -> ISet.forall ((=) 1) nullSet |> ignore)) + $"%s{nameof ISet.intersect} (set1)", struct ("set1", (fun () -> ISet.intersect nullSet set |> ignore)) + $"%s{nameof ISet.intersect} (set2)", struct ("set2", (fun () -> ISet.intersect set nullSet |> ignore)) + nameof ISet.intersectMany, struct ("sets", (fun () -> ISet.intersectMany [| set; nullSet |] |> ignore)) + nameof ISet.isEmpty, struct ("set", (fun () -> ISet.isEmpty nullSet |> ignore)) + $"%s{nameof ISet.isProperSubset} (set1)", struct ("set1", (fun () -> ISet.isProperSubset nullSet set |> ignore)) + $"%s{nameof ISet.isProperSubset} (set2)", struct ("set2", (fun () -> ISet.isProperSubset set nullSet |> ignore)) + $"%s{nameof ISet.isProperSuperset} (set1)", struct ("set1", (fun () -> ISet.isProperSuperset nullSet set |> ignore)) + $"%s{nameof ISet.isProperSuperset} (set2)", struct ("set2", (fun () -> ISet.isProperSuperset set nullSet |> ignore)) + $"%s{nameof ISet.isSubset} (set1)", struct ("set1", (fun () -> ISet.isSubset nullSet set |> ignore)) + $"%s{nameof ISet.isSubset} (set2)", struct ("set2", (fun () -> ISet.isSubset set nullSet |> ignore)) + $"%s{nameof ISet.isSuperset} (set1)", struct ("set1", (fun () -> ISet.isSuperset nullSet set |> ignore)) + $"%s{nameof ISet.isSuperset} (set2)", struct ("set2", (fun () -> ISet.isSuperset set nullSet |> ignore)) + nameof ISet.iter, struct ("set", (fun () -> ISet.iter ignore nullSet)) + nameof ISet.map, struct ("set", (fun () -> ISet.map id nullSet |> ignore)) + $"%s{nameof ISet.overlaps} (set1)", struct ("set1", (fun () -> ISet.overlaps nullSet set |> ignore)) + $"%s{nameof ISet.overlaps} (set2)", struct ("set2", (fun () -> ISet.overlaps set nullSet |> ignore)) + nameof ISet.partition, struct ("set", (fun () -> ISet.partition ((=) 1) nullSet |> ignore)) + nameof ISet.partitionWith, + struct ("set", + (fun () -> + ISet.partitionWith (fun (item : int) -> Choice.Choice1Of2 item) nullSet + |> ignore + )) + nameof ISet.remove, struct ("set", (fun () -> ISet.remove 1 nullSet |> ignore)) + nameof ISet.removeRange, struct ("set", (fun () -> ISet.removeRange [| 1 |] nullSet |> ignore)) + $"%s{nameof ISet.setEquals} (set1)", struct ("set1", (fun () -> ISet.setEquals nullSet set |> ignore)) + $"%s{nameof ISet.setEquals} (set2)", struct ("set2", (fun () -> ISet.setEquals set nullSet |> ignore)) + $"%s{nameof ISet.symmetricDifference} (set1)", + struct ("set1", (fun () -> ISet.symmetricDifference nullSet set |> ignore)) + $"%s{nameof ISet.symmetricDifference} (set2)", + struct ("set2", (fun () -> ISet.symmetricDifference set nullSet |> ignore)) + nameof ISet.toArray, struct ("set", (fun () -> ISet.toArray nullSet |> ignore)) + nameof ISet.toList, struct ("set", (fun () -> ISet.toList nullSet |> ignore)) + nameof ISet.toSeq, struct ("set", (fun () -> ISet.toSeq nullSet |> ignore)) + nameof ISet.tryGetValue, struct ("set", (fun () -> ISet.tryGetValue 1 nullSet |> ignore)) + $"%s{nameof ISet.union} (set1)", struct ("set1", (fun () -> ISet.union nullSet set |> ignore)) + $"%s{nameof ISet.union} (set2)", struct ("set2", (fun () -> ISet.union set nullSet |> ignore)) + nameof ISet.unionMany, struct ("sets", (fun () -> ISet.unionMany [| nullSet; set |] |> ignore)) + |] + + /// A flat list of hash sets as a sequence of interface sets. A flat list of hash sets is also a sequence of interface + /// sets, 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 interface sets does not see it. + static let asInterfaceSets (sets : FlatList>) : seq> = + match box sets with + | :? seq> as widened -> widened + | _ -> invalidOp "A flat list of hash sets must be a sequence of interface sets" + + /// + /// 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 the sequence (ADR 0001, D6). + /// + static let sequenceRejections : Map unit))> = + let set = HashSet.ofArray Inputs.Several :> IImmutableSet + let nullElements = Unchecked.defaultof> + let defaultElements = Unchecked.defaultof> :> seq + let nullSets = Unchecked.defaultof>> + let defaultSets = Unchecked.defaultof>> :> seq> + + // The flat lists of strings and of hash sets are sequences of objects and of interface sets + let widenedElements = FlatListOracles.defaultStringsAsObjects + let objects = HashSet.ofArray [| ("a" :> obj) |] :> IImmutableSet + let widenedSets = asInterfaceSets Unchecked.defaultof>> + + Map.ofArray [| + $"%s{nameof ISet.addRange} (null elements)", struct ("elements", (fun () -> ISet.addRange nullElements set |> ignore)) + $"%s{nameof ISet.addRange} (default flat list)", + struct ("elements", (fun () -> ISet.addRange defaultElements set |> ignore)) + $"%s{nameof ISet.addRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> ISet.addRange widenedElements objects |> ignore)) + $"%s{nameof ISet.intersectMany} (null sets)", struct ("sets", (fun () -> ISet.intersectMany nullSets |> ignore)) + $"%s{nameof ISet.intersectMany} (default flat list)", + struct ("sets", (fun () -> ISet.intersectMany defaultSets |> ignore)) + $"%s{nameof ISet.intersectMany} (default flat list of hash sets as interface sets)", + struct ("sets", (fun () -> ISet.intersectMany widenedSets |> ignore)) + $"%s{nameof ISet.removeRange} (null elements)", + struct ("elements", (fun () -> ISet.removeRange nullElements set |> ignore)) + $"%s{nameof ISet.removeRange} (default flat list)", + struct ("elements", (fun () -> ISet.removeRange defaultElements set |> ignore)) + $"%s{nameof ISet.removeRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> ISet.removeRange widenedElements objects |> ignore)) + $"%s{nameof ISet.unionMany} (null sets)", struct ("sets", (fun () -> ISet.unionMany nullSets |> ignore)) + $"%s{nameof ISet.unionMany} (default flat list)", struct ("sets", (fun () -> ISet.unionMany defaultSets |> ignore)) + $"%s{nameof ISet.unionMany} (default flat list of hash sets as interface sets)", + struct ("sets", (fun () -> ISet.unionMany widenedSets |> ignore)) + |] + + /// + /// The relations of a set with a sequence, by name: the member of the interface, which takes the sequence, and the + /// function of the module, which takes a set. + /// + static let relationFunctions + : Map< + string, + struct ((IImmutableSet -> seq -> bool) * (IImmutableSet -> IImmutableSet -> bool)) + > = + Map.ofArray [| + nameof ISet.isProperSubset, struct ((fun set elements -> set.IsProperSubsetOf elements), ISet.isProperSubset) + nameof ISet.isProperSuperset, struct ((fun set elements -> set.IsProperSupersetOf elements), ISet.isProperSuperset) + nameof ISet.isSubset, struct ((fun set elements -> set.IsSubsetOf elements), ISet.isSubset) + nameof ISet.isSuperset, struct ((fun set elements -> set.IsSupersetOf elements), ISet.isSuperset) + nameof ISet.overlaps, struct ((fun set elements -> set.Overlaps elements), ISet.overlaps) + nameof ISet.setEquals, struct ((fun set elements -> set.SetEquals elements), ISet.setEquals) + |] + + /// + /// The operations of the set algebra with a sequence, by name: the member of the interface, which takes the sequence, + /// and the function of the module, which takes a set. + /// + static let operationFunctions + : Map< + string, + struct ((IImmutableSet -> seq -> IImmutableSet) * + (IImmutableSet -> IImmutableSet -> IImmutableSet)) + > = + Map.ofArray [| + nameof ISet.intersect, struct ((fun set elements -> set.Intersect elements), ISet.intersect) + nameof ISet.symmetricDifference, struct ((fun set elements -> set.SymmetricExcept elements), ISet.symmetricDifference) + |] + + /// The relations that need no conversion, by name, as the changelog gives them: each tests the elements with contains. + static let streamingRecipes : Map -> seq -> bool> = + Map.ofArray [| + nameof ISet.isSuperset, (fun set elements -> Seq.forall (fun item -> ISet.contains item set) elements) + nameof ISet.overlaps, (fun set elements -> Seq.exists (fun item -> ISet.contains item set) elements) + |] + + /// The names of the transformations that keep the element type, as rows. + static member SameTypeTransformations = sameTypeTransformations.Keys + + /// The names of the transformations that keep the comparer, as rows. + static member ComparerKeepers = comparerKeepers.Keys + + /// The names of the transformations to another element type, as rows. + static member TypeChanges = typeChanges.Keys + + /// The names of the functions that take a set, with the parameter where a function takes two, as rows. + static member NullRejections = nullRejections.Keys + + /// The names of the functions that take a sequence, with the input that they reject, as rows. + static member SequenceRejections = sequenceRejections.Keys + + [] + member _.``the transformations that keep the element type keep the implementation`` (name : string) = + let result = sameTypeTransformations[name] (sortedSet Inputs.Several) (sortedSet Inputs.Other) + + let sorted = + Assert.IsInstanceOfType>(result, $"ISet.%s{name} must keep the implementation of its input") + + CollectionAssert.AreEqual ( + Array.sort (Seq.toArray sorted), + Seq.toArray sorted, + $"ISet.%s{name} must keep the order of the sorted set" + ) + + [] + member _.``the transformations that keep the element type keep the comparer`` (name : string) = + Assert.AreSame ( + ignoreCase, + comparerOf (comparerKeepers[name](words ())) $"ISet.%s{name} must keep the hash set of its input", + $"ISet.%s{name} must keep the comparer" + ) + + [] + member _.``isProperSuperset of a sorted set counts the elements that its comparer finds equal once`` () = + // The sorted set orders by remainders, so it holds one element of each of the five remainders of its input; the + // second set holds six elements with three remainders (ADR 0001, D14) + let sorted = + ImmutableSortedSet.CreateRange(SetInputs.ByRemainder, Inputs.Other) :> IImmutableSet + + Assert.IsTrue ( + ISet.isProperSuperset sorted (HashSet.ofArray Inputs.Several), + "ISet.isProperSuperset must count the elements that the comparer of a sorted set finds equal once" + ) + + [] + member _.``addRange compares with the comparer of the set`` () = + // "APPLE" equals "apple" without case, so the set keeps "apple" (ADR 0001, D14) + CollectionAssert.AreEqual ( + [| "Banana"; "apple"; "cherry"; "date" |], + Array.sort (ISet.toArray (ISet.addRange [| "APPLE"; "date" |] (words ()))), + "ISet.addRange must compare with the comparer of the set" + ) + + [] + member _.``removeRange compares with the comparer of the set`` () = + CollectionAssert.AreEqual ( + [| "Banana"; "cherry" |], + Array.sort (ISet.toArray (ISet.removeRange [| "APPLE"; "date" |] (words ()))), + "ISet.removeRange must compare with the comparer of the set" + ) + + // The hash set answers an empty input that receives a hash set with the argument or a copy of it, and the interface + // promises no instance, so the function decides by the count (ADR 0001, D14) + [] + member _.``addRange returns an empty input for an empty hash set`` () = + let set = (HashSet.forComparer ignoreCase).empty :> IImmutableSet + + Assert.AreSame (set, ISet.addRange HashSet.empty set, "ISet.addRange of an empty hash set must return the input") + + [] + member _.``union compares with the comparer of the first set`` () = + Assert.HasCount ( + 4, + ISet.union (words ()) (HashSet.ofArray [| "APPLE"; "date" |]), + "ISet.union must compare with the comparer of the first set" + ) + + [] + member _.``the transformations to another element type return hash sets with the default comparer`` (name : string) = + Assert.AreSame ( + defaultComparer, + HashSet.comparer (typeChanges[name](words ())), + $"ISet.%s{name} must compare with the default comparer" + ) + + [] + member _.``map of a sorted set returns a hash set of the results`` () = + let fromSorted : HashSet = ISet.map id (sortedSet Inputs.Several) + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray Inputs.Several), + HashSet.toArray fromSorted |> Array.sort, + "ISet.map of a sorted set must return a hash set of the results" + ) + + [] + member _.``unionMany of no sets is an empty hash set`` () = + let union = ISet.unionMany Seq.empty> + let hashSet = + Assert.IsInstanceOfType>(union, "ISet.unionMany of no sets must return a hash set") + Assert.IsEmpty (hashSet, "ISet.unionMany of no sets must be empty") + + [] + member _.``tryGetValue returns the element that the set holds`` () = + Assert.AreEqual( + ValueSome "apple", + ISet.tryGetValue "APPLE" (words ()), + "ISet.tryGetValue must compare without case" + ) + Assert.AreEqual( + ValueNone, + ISet.tryGetValue "date" (words ()), + "ISet.tryGetValue must return nothing for a missing value" + ) + Assert.AreEqual( + ValueSome 5, + ISet.tryGetValue 5 (sortedSet Inputs.Several), + "ISet.tryGetValue must search a sorted set" + ) + + [] + member _.``functions reject a null set`` (name : string) = + let struct (paramName, actual) = nullRejections[name] + + let ex = + Assert.ThrowsExactly(Action actual, $"ISet.%s{name} must reject a null set") + + Assert.AreEqual(paramName, ex.ParamName, $"ISet.%s{name} must name the parameter of the set") + + // The way to pass a sequence to a relation, as the changelog gives it: the interface cannot tell the comparer of the + // first set, so the sequence is added to the empty set that clear returns, which keeps the implementation and the + // comparer. The conversion with the default comparer gives the same answer when the comparer of the first set finds equal + // what the default comparer finds equal, and another answer when it tells such strings apart + [)>] + member _.``a relation with a sequence takes the sequence that is added to the empty set of the first set`` + (relation : string, comparer : string) + = + let instances = SetRelationInputs.Strings [| "a1"; "a2" |] + Assert.AreNotSame ( + instances[0], + instances[1], + "The test needs two strings with the same text that are different instances" + ) + + let struct (setItems, sequenceItems) = SetRelationInputs.Items[relation] + let struct (interfaceMember, moduleFunction) = relationFunctions[relation] + let struct (firstComparer, defaultFindsEqualWhatItFindsEqual) = SetRelationInputs.Comparers[comparer] + + let set = + (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) :> IImmutableSet + + let elements = SetRelationInputs.Strings sequenceItems + let expected = interfaceMember set elements + + Assert.AreEqual( + expected, + moduleFunction set (ISet.addRange elements (ISet.clear set)), + $"ISet.%s{relation} must answer as the member of the interface for the sequence that is added to the empty set of the first set" + ) + + let withDefaultComparer = moduleFunction set (HashSet.ofSeq elements) + + if defaultFindsEqualWhatItFindsEqual then + Assert.AreEqual( + expected, + withDefaultComparer, + $"ISet.%s{relation} must answer as the member of the interface for the sequence that the default comparer converts, because the comparer of the first set finds equal what the default comparer finds equal" + ) + else + Assert.AreNotEqual( + expected, + withDefaultComparer, + $"ISet.%s{relation} must answer differently for the sequence that the default comparer converts, because the comparer of the first set tells apart strings that the default comparer finds equal" + ) + + // The same recipe for the operations of the set algebra that take a sequence in the interface + [)>] + member _.``an operation with a sequence takes the sequence that is added to the empty set of the first set`` + (operation : string, comparer : string) + = + let struct (setItems, sequenceItems) = SetRelationInputs.OperationItems[operation] + let struct (interfaceMember, moduleFunction) = operationFunctions[operation] + let struct (firstComparer, defaultFindsEqualWhatItFindsEqual) = SetRelationInputs.Comparers[comparer] + + let set = + (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) :> IImmutableSet + + let elements = SetRelationInputs.Strings sequenceItems + let expected = ISet.toArray (interfaceMember set elements) + + CollectionAssert.AreEquivalent ( + expected, + ISet.toArray (moduleFunction set (ISet.addRange elements (ISet.clear set))), + $"ISet.%s{operation} must return the result of the member of the interface for the sequence that is added to the empty set of the first set" + ) + + let withDefaultComparer = ISet.toArray (moduleFunction set (HashSet.ofSeq elements)) + + if defaultFindsEqualWhatItFindsEqual then + CollectionAssert.AreEquivalent ( + expected, + withDefaultComparer, + $"ISet.%s{operation} must return the result of the member of the interface for the sequence that the default comparer converts, because the comparer of the first set finds equal what the default comparer finds equal" + ) + else + CollectionAssert.AreNotEquivalent ( + expected, + withDefaultComparer, + $"ISet.%s{operation} must return another result for the sequence that the default comparer converts, because the comparer of the first set tells apart strings that the default comparer finds equal" + ) + + [)>] + member _.``overlaps and isSuperset with a sequence test each element with contains`` (relation : string, comparer : string) = + let struct (setItems, sequenceItems) = SetRelationInputs.Items[relation] + let struct (interfaceMember, _) = relationFunctions[relation] + let struct (firstComparer, _) = SetRelationInputs.Comparers[comparer] + + let set = + (HashSet.forComparer firstComparer).ofArray(SetRelationInputs.Strings setItems) :> IImmutableSet + + let elements = SetRelationInputs.Strings sequenceItems + + Assert.AreEqual( + interfaceMember set elements, + streamingRecipes[relation] set elements, + $"%s{relation} with a sequence must test each element with ISet.contains, in the comparer of the first set" + ) + + [] + member _.``functions that take a sequence reject a null sequence and a default flat list given as the sequence`` + (name : string) + = + let struct (paramName, actual) = sequenceRejections[name] + FlatListAssert.RejectsNull ($"ISet.%s{name}", paramName, actual) + + [] + member _.``a flat list that is not default passes as a sequence of objects and as a sequence of interface sets`` () = + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + let objects = HashSet.ofArray [| ("b" :> obj); ("c" :> obj) |] :> IImmutableSet + + let sets = + asInterfaceSets (FlatList.ofArray [| HashSet.ofArray [| 1; 2 |]; HashSet.ofArray [| 2; 3 |] |]) + + CollectionAssert.AreEquivalent ( + [| ("a" :> obj); ("b" :> obj); ("c" :> obj) |], + ISet.toArray (ISet.addRange widened objects), + "ISet.addRange must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| ("c" :> obj) |], + ISet.toArray (ISet.removeRange widened objects), + "ISet.removeRange must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEquivalent ( + [| 1; 2; 3 |], + ISet.toArray (ISet.unionMany sets), + "ISet.unionMany must take the sets of a flat list of hash sets as interface sets" + ) + + CollectionAssert.AreEquivalent ( + [| 2 |], + ISet.toArray (ISet.intersectMany sets), + "ISet.intersectMany must take the sets of a flat list of hash sets as interface sets" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTupleTests.fs new file mode 100644 index 00000000..7bd595cf --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTupleTests.fs @@ -0,0 +1,62 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Immutable +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 `ISet` module of the `Tuple` namespace. +[] +type ISetTupleTests () = + + static let numbers = HashSet.ofArray Inputs.Several :> IImmutableSet + + static let expected = Set.ofArray Inputs.Several + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : IImmutableSet), (rejected : IImmutableSet) = + ISet.partition (fun item -> item > 0) numbers + let expectedAccepted, expectedRejected = Set.partition (fun item -> item > 0) expected + + CollectionAssert.AreEqual ( + Set.toArray expectedAccepted, + ISet.toArray accepted |> Array.sort, + "Tuple.ISet.partition must return the accepted elements" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedRejected, + ISet.toArray rejected |> Array.sort, + "Tuple.ISet.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 : HashSet), (second : HashSet) = ISet.partitionWith partitioner numbers + let expectedFirst, expectedSecond = Set.partitionWith partitioner expected + + CollectionAssert.AreEqual ( + Set.toArray expectedFirst, + HashSet.toArray first |> Array.sort, + "Tuple.ISet.partitionWith must return the first choices" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedSecond, + HashSet.toArray second |> Array.sort, + "Tuple.ISet.partitionWith must return the second choices" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = ISet.tryGetValue 8 numbers + Assert.AreEqual(ValueSome 8, found, "ISet.tryGetValue must resolve to the default module")