From ae5db1c59e1607cd7ffcfe48270c8e29a688163c Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:56:02 +0200 Subject: [PATCH 1/4] feat: `SortedSet` module with the vocabulary of the FSharp.Core `Set` module Rewrites `SortedSet` after ADR 0001 in `SortedSet/SortedSet.fsi` and `SortedSet/SortedSet.fs`: 51 functions in the canonical category order, each documented in the signature file, with the names, the argument order and the exceptions of the `Set` module. - Wherever no comparer is given, the sorted sets compare with F# generic comparison, the comparer of `Set`, instead of `Comparer<'T>.Default`: creation, the builders, `sortedSet { }` and the results of `map`, `choose` and `partitionWith`. A sorted set enumerates its elements in the order of the F# set of the same elements, and `foldBack` visits them in descending order. - `SortedSets.DefaultComparer` in `Helper.fs` resolves that comparer once per type. `FastGenericComparer` creates a new comparer at each call with a concrete type that is not primitive, and an optimized build makes such calls when it inlines a small function of the module into its caller. The sorted sets of one type share one comparer, so the shortcuts of `ImmutableSortedSet` for two sets with the same comparer apply. - The creation functions add the elements to a builder, which keeps the first of the elements that compare equal as `Set.ofSeq` does; `ImmutableSortedSet.CreateRange` sorts unstably and can keep any. - `minElement` and `maxElement` throw for an empty sorted set as `Set` does. `item`, `tryItem`, `indexOf` and `rev` wrap the indexer, `IndexOf` and `Reverse` and throw what they throw. - `filter`, `partition`, `add`, `remove`, `clear` and the set algebra keep the comparer of their input, the set algebra that of the first set; `intersect` keeps the elements of the second set, as the BCL does. - `tryGetValue`, `tryItem` and `choose` use `voption` and `partition` and `partitionWith` struct tuples; the `Option` and `Tuple` namespaces repeat them with options and reference tuples. - `sortedSet { }` collects its elements in a builder; `SortedSetCE` created with a comparer builds sorted sets that order with it. - `Helper.fs` gets `ErrorStrings.SetContainsNoElements` and the `SortedSets` module; `intersectMany` and `unionMany` reuse `Sets.reduce`. - The legacy `Sets.fs`, with the old `SortedSet` module and the `SortedSetBuilder` abbreviation, is removed. Co-Authored-By: Claude Opus 5.5 --- .../ComputationExpressions.fs | 3 + .../ComputationExpressions.fsi | 28 + .../FSharp.Collections.Immutable.fsproj | 17 +- .../Helper/ErrorStrings.fs | 4 + .../Helper/SortedSets.fs | 76 ++ src/FSharp.Collections.Immutable/Sets.fs | 131 --- .../SortedSet/SortedSet.CustomComparer.fs | 90 ++ .../SortedSet/SortedSet.CustomComparer.fsi | 206 ++++ .../SortedSet/SortedSet.fs | 399 ++++++++ .../SortedSet/SortedSet.fsi | 879 ++++++++++++++++++ .../SortedSet/SortedSetCE.fs | 91 ++ .../SortedSet/SortedSetCE.fsi | 93 ++ .../SortedSet/SortedSetOption.fs | 24 + .../SortedSet/SortedSetOption.fsi | 61 ++ .../SortedSet/SortedSetTuple.fs | 18 + .../SortedSet/SortedSetTuple.fsi | 51 + .../SortedSet/SortedSetTypes.fs | 17 + 17 files changed, 2056 insertions(+), 132 deletions(-) create mode 100644 src/FSharp.Collections.Immutable/Helper/SortedSets.fs delete mode 100644 src/FSharp.Collections.Immutable/Sets.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fsi create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSet.fsi create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fsi create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fsi create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fsi create mode 100644 src/FSharp.Collections.Immutable/SortedSet/SortedSetTypes.fs diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 156d4eb5..636342a1 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fs +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -7,3 +7,6 @@ let flatList = FlatListCE () [] let hashSet<'T> = HashSetCE<'T>() + +[] +let sortedSet<'T when 'T : comparison> = SortedSetCE<'T>() diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi index 78396635..328b923d 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -46,3 +46,31 @@ val flatList : FlatListCE /// [] val hashSet<'T> : HashSetCE<'T> + +/// +/// Builds a sorted set that compares with F# generic comparison. +/// +/// +/// The value is a type function, so that one value builds sorted sets of every element type. The sorted set enumerates +/// its elements in the order of the F# set of the same elements. A +/// created with a comparer builds sorted sets that order +/// their elements with it. +/// +/// +/// +/// let squares = sortedSet { +/// for i in -2..2 do +/// yield i * i +/// yield! [ 9; 4 ] +/// } +/// +/// Evaluates to a sorted set of 0, 1, 4 and 9, in this order. +/// +/// +/// +/// let names = SortedSetCE StringComparer.OrdinalIgnoreCase { "bob"; "Ann"; "ANN" } +/// +/// Evaluates to a sorted set of "Ann" and "bob" that ignores case. +/// +[] +val sortedSet<'T when 'T : comparison> : SortedSetCE<'T> diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index ee70cb54..3285bc73 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -35,6 +35,7 @@ + @@ -67,12 +68,26 @@ + + + SortedSet.fsi + + + SortedSet.fs + + + + + + + + + - diff --git a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs index 1e42ecec..cd779068 100644 --- a/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs +++ b/src/FSharp.Collections.Immutable/Helper/ErrorStrings.fs @@ -69,3 +69,7 @@ let NotEnoughElements = "The input sequence has an insufficient number of elemen /// FSCore.resx, outOfRange. [] let OutOfRange = "The index is outside the legal range." + +/// FSCore.resx, setContainsNoElements. +[] +let SetContainsNoElements = "Set contains no elements." diff --git a/src/FSharp.Collections.Immutable/Helper/SortedSets.fs b/src/FSharp.Collections.Immutable/Helper/SortedSets.fs new file mode 100644 index 00000000..5ee69f3c --- /dev/null +++ b/src/FSharp.Collections.Immutable/Helper/SortedSets.fs @@ -0,0 +1,76 @@ +/// +/// Algorithms that build sorted sets one element at a time, for the creation members of CustomComparerSortedSet +/// and the transformations of the SortedSet module that change the element type (ADR 0001, D5). +/// +/// +/// +/// A builder keeps the first of the elements that compare equal, as the FSharp.Core Set module does. +/// ImmutableSortedSet.CreateRange sorts the elements with an unstable sort before it drops the duplicates, so it can +/// keep any of them. +/// +/// +/// The transformations that change the element type compare the new elements with F# generic comparison, +/// DefaultComparer of the Comparers module, the comparer of the sorted sets created without one (D5, +/// Comparers): the comparer of the input cannot compare elements of another type. +/// +/// +module internal FSharp.Collections.Immutable.SortedSets + +open System.Collections.Generic +open System.Collections.Immutable + +/// +/// The distinct elements of a sequence in a sorted set with the comparer, the first of the elements that compare equal. +/// +/// +/// The caller checks the input: checkNotNullOrDefaultSeq for a sequence, which also rejects a default flat list +/// given as a sequence, and checkNotNull for an array or a list. The exception then names the parameter of the +/// caller and not the parameter of UnionWith (ADR 0001, D6). +/// +let ofSeq (comparer : IComparer<'T>) (source : seq<'T>) : ImmutableSortedSet<'T> = + let builder = ImmutableSortedSet.CreateBuilder<'T> comparer + builder.UnionWith source + builder.ToImmutable () + +/// The values that the chooser returns for the elements. +let inline choose ([] chooser : 'T -> 'U voption) (source : ImmutableSortedSet<'T>) : ImmutableSortedSet<'U> = + let builder = ImmutableSortedSet.CreateBuilder<'U> DefaultComparer<'U>.Instance + + 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 : ImmutableSortedSet<'T>) : ImmutableSortedSet<'U> = + let builder = ImmutableSortedSet.CreateBuilder<'U> DefaultComparer<'U>.Instance + + 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, +/// which it visits in descending order. +/// +/// +/// Set.partitionWith visits the elements from the largest one and keeps the first of the values that compare +/// equal, and so does a builder: the same one of two equal values stays. +/// +let inline partitionWith + ([] partitioner : 'T -> Choice<'T1, 'T2>) + (source : ImmutableSortedSet<'T>) + : struct (ImmutableSortedSet<'T1> * ImmutableSortedSet<'T2>) = + let first = ImmutableSortedSet.CreateBuilder<'T1> DefaultComparer<'T1>.Instance + let second = ImmutableSortedSet.CreateBuilder<'T2> DefaultComparer<'T2>.Instance + + // Reverse walks the tree from the largest element without a copy + for item in source.Reverse () 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/Sets.fs b/src/FSharp.Collections.Immutable/Sets.fs deleted file mode 100644 index 563d94e7..00000000 --- a/src/FSharp.Collections.Immutable/Sets.fs +++ /dev/null @@ -1,131 +0,0 @@ -namespace FSharp.Collections.Immutable - -type SortedSet<'T> = System.Collections.Immutable.ImmutableSortedSet<'T> -type SortedSetBuilder<'T> = SortedSet<'T>.Builder - -[] -module SortedSet = - - type internal SortedSetFactory = System.Collections.Immutable.ImmutableSortedSet - - let inline check (sortedSet : SortedSet<_>) = checkNotNull (nameof sortedSet) sortedSet - - ////////// Creating ////////// - - let inline empty<'T> = SortedSetFactory.Create<'T>() - let inline singleton<'T> (item : 'T) = SortedSetFactory.Create<'T>(item) - let inline ofSeq source = SortedSetFactory.CreateRange (source) - let inline ofSeqWithComparer comparer source = SortedSetFactory.Create (comparer, items = (source |> Array.ofSeq)) - let inline ofArray (source : _ array) = SortedSetFactory.CreateRange (source) - - let inline ofBuilder (sortedSetBuilder : SortedSetBuilder<_>) = - checkNotNull (nameof sortedSetBuilder) sortedSetBuilder - sortedSetBuilder.ToImmutable () - - let inline ofComparer<'T> comparer = SortedSetFactory.Create<'T>(comparer = comparer) - - let inline toSeq (set : SortedSet<_>) = set :> seq<_> - - let inline toArray (set : SortedSet<_>) = - check set - Seq.toArray set - - ////////// Building ////////// - - let inline builder () = SortedSetFactory.CreateBuilder () - let inline builderWith capacity : SortedSet<'T>.Builder = SortedSetFactory.CreateBuilder (capacity) - let inline builderWithComparer comparer = SortedSetFactory.CreateBuilder (comparer) - - let inline toBuilder set : SortedSetBuilder<_> = - check set - set.ToBuilder () - - let inline keyComparer set = - check set - set.KeyComparer - - - let inline length set = - check set - set.Count - - let inline contains value set = - check set - set.Contains value - - let inline isEmpty set = - check set - set.IsEmpty - - let inline exists predicate map = - check map - map |> Seq.exists predicate - - let inline isSubset (set1 : SortedSet<_>) set2 = - check set1 - set1.IsSubsetOf set2 - - let inline isProperSubset (set1 : SortedSet<_>) set2 = - check set1 - set1.IsProperSubsetOf set2 - - let inline isSuperset (set1 : SortedSet<_>) set2 = - check set1 - set1.IsSupersetOf set2 - - let inline isProperSuperset (set1 : SortedSet<_>) set2 = - check set1 - set1.IsProperSupersetOf set2 - - let inline add value set : SortedSet<_> = - check set - set.Add (value) - - let inline union set values : SortedSet<_> = - check set - values |> set.Union - - let inline unionMany (sets : SortedSet<_> seq) = Seq.reduce union sets - let inline intersect (set1 : SortedSet<_>) set2 = set1.Intersect set2 - let inline intersectMany (sets : SortedSet<_> seq) = Seq.reduce intersect sets - - let inline remove value set : SortedSet<_> = - check set - set.Remove value - - let inline difference values set : SortedSet<_> = - check set - values |> set.Except - - let inline clear set : SortedSet<_> = - 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 : SortedSet<_>) = set |> Seq.map mapping |> ofSeq - - let inline forall predicate set = set |> Seq.forall predicate - - let inline iter action (set : SortedSet<_>) = - check set - set |> Seq.iter action diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fs new file mode 100644 index 00000000..5611152e --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fs @@ -0,0 +1,90 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable +// Opened after System.Collections.Generic, so that SortedSet 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 Comparer<'T>.Default, and the Comparer property does the same: F# generic comparison would need the +// comparison constraint, which the struct cannot require, because a custom comparer serves types without F# comparison. +[] +type CustomComparerSortedSet<'T> = + + val private comparer : IComparer<'T> | null + + new (comparer : IComparer<'T>) + = + checkNotNull (nameof comparer) comparer + { comparer = comparer } + + // ---- Creation ---- + + [] + member this.empty : SortedSet<'T> = ImmutableSortedSet.Create<'T> this.Comparer + + // The elements go one by one into a builder, which keeps the first of the elements that compare equal, as Set.ofSeq + // does; ImmutableSortedSet.CreateRange sorts them unstably and can keep any of them (SortedSets.ofSeq) + + [] + member this.ofArray (array : 'T array) : SortedSet<'T> = + checkNotNull (nameof array) array + SortedSets.ofSeq this.Comparer array + + [] + member this.ofList (elements : 'T list) : SortedSet<'T> = + // Set.ofList dereferences a null list without a check; a null input throws ArgumentNullException here (D6) + checkNotNull (nameof elements) elements + SortedSets.ofSeq this.Comparer elements + + // ImmutableSortedSet.Create with a span sorts the elements unstably and keeps an arbitrary one of the elements that the + // comparer finds equal (measured: 32 of 100 first spellings kept, against 100 of 100 for a builder loop), so the + // elements go into a builder one by one, as in ofArray. A byref-like parameter cannot reach SortedSets.ofSeq. + [] + member this.ofReadOnlySpan (span : ReadOnlySpan<'T>) : SortedSet<'T> = + let builder = ImmutableSortedSet.CreateBuilder<'T> this.Comparer + + for item in span do + builder.Add item |> ignore + + builder.ToImmutable () + + [] + member this.ofSeq (elements : seq<'T>) : SortedSet<'T> = + // A default flat list given as a sequence would fail with InvalidOperationException in the builder (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof elements) elements + let comparer = this.Comparer + + match elements with + // A sorted set that orders with the same comparer instance is returned as it is, as ImmutableSortedSet.WithComparer + // returns it (the test is reference equality, ADR 0001, D14). A sorted set with another comparer goes through the + // builder like any sequence: WithComparer would refill the tree with an unstable sort. + | :? SortedSet<'T> as set when obj.ReferenceEquals (set.KeyComparer, comparer) -> set + | _ -> SortedSets.ofSeq comparer elements + + // The span of the argument is converted to a read-only span by the call + [] + member this.ofSpan (span : Span<'T>) : SortedSet<'T> = this.ofReadOnlySpan span + + [] + member this.singleton (value : 'T) : SortedSet<'T> = ImmutableSortedSet.Create<'T>(this.Comparer, value) + + // ---- Builder ---- + + [] + member this.build (action : SortedSet<'T>.Builder -> unit) : SortedSet<'T> = + let builder = ImmutableSortedSet.CreateBuilder<'T> this.Comparer + action builder + builder.ToImmutable () + + [] + member this.builder () : SortedSet<'T>.Builder = ImmutableSortedSet.CreateBuilder<'T> this.Comparer + + // ---- Properties ---- + + member this.Comparer : IComparer<'T> = + match this.comparer with + | null -> Comparer<'T>.Default :> IComparer<'T> + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fsi b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fsi new file mode 100644 index 00000000..fc065b25 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.CustomComparer.fsi @@ -0,0 +1,206 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +// Opened after System.Collections.Generic, so that SortedSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +/// +/// Creates sorted sets that order their elements with a 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. The element type needs no F# comparison, because +/// the comparer orders the elements. For the same reason, the default value of the struct, which no constructor creates, +/// cannot fall back to F# generic comparison: it orders with , +/// as the BCL does for a comparer. +/// +/// +/// +/// +/// let names = (SortedSet.forComparer StringComparer.OrdinalIgnoreCase).ofList [ "bob"; "Ann"; "ANN" ] +/// +/// Evaluates to a sorted set of "Ann" and "bob", in this order, that compares without case. +/// +[] +type CustomComparerSortedSet<'T> = + + // A signature reveals the fields of a struct, private ones included (FS0314); null only in the default value + val private comparer : IComparer<'T> | null + + /// Creates the struct for a comparer. + /// The comparer that orders the elements of the sorted sets that the struct creates. + /// Thrown when is . + new : comparer : IComparer<'T> -> CustomComparerSortedSet<'T> + + // ---- Creation ---- + + /// + /// The empty sorted set that orders with the comparer. + /// + /// + /// O(1). The member wraps + /// . + /// + [] + member empty : SortedSet<'T> + + /// + /// Creates a sorted set from the elements of an array, ordered by the comparer. + /// + /// The input array. + /// + /// The sorted 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 sorted set. The member adds the elements one by one to a + /// builder that + /// + /// creates, as + /// does. + /// + [] + member ofArray : array : 'T array -> SortedSet<'T> + + /// + /// Creates a sorted set from the elements of a list, ordered by the comparer. + /// + /// The input list. + /// + /// The sorted 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). The member adds the elements one by one to a builder, as + /// + /// does. + /// + [] + member ofList : elements : 'T list -> SortedSet<'T> + + /// + /// Creates a sorted set from the elements of a read-only span, ordered by the comparer. + /// + /// The input span. + /// + /// The sorted 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 sorted set. + /// + /// + /// + /// O(n log n). Later changes to the memory under the span do not change the sorted set. The member adds the elements + /// one by one to a builder that + /// + /// creates. + /// + /// + /// It does not wrap + /// , + /// which sorts the elements with an unstable sort and can keep any of the elements that compare equal. + /// + /// + [] + member ofReadOnlySpan : span : ReadOnlySpan<'T> -> SortedSet<'T> + + /// + /// Creates a sorted set from the elements of a sequence, ordered by the comparer. + /// + /// The input sequence, which can be a sorted set with another comparer. + /// + /// The sorted 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 adds the elements one by one to a builder that + /// + /// creates. It does not call + /// , + /// which sorts the elements with an unstable sort and can keep any of the elements that compare equal. + /// + /// + /// A sorted set gets the comparer, as + /// + /// gives it: the member returns the sorted set itself in O(1) when it orders with the same comparer instance, an + /// empty one included, and otherwise a new sorted set of its elements in the comparer. The BCL tests the instance, not + /// the behaviour of the comparer, so an equal but distinct instance rebuilds the sorted set (ADR 0001, D14). + /// + /// + [] + member ofSeq : elements : seq<'T> -> SortedSet<'T> + + /// + /// Creates a sorted set from the elements of a span, ordered by the comparer. + /// + /// The input span. + /// + /// The sorted 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 sorted set. + /// + /// + /// O(n log n). Later changes to the memory under the span do not change the sorted set. The member converts the span + /// to a read-only span and does what + /// + /// does. + /// + [] + member ofSpan : span : Span<'T> -> SortedSet<'T> + + /// + /// Creates a sorted set of one element that orders with the comparer. + /// + /// The element. + /// The sorted set of one element. + /// + /// O(1). The member wraps + /// . + /// + [] + member singleton : value : 'T -> SortedSet<'T> + + // ---- Builder ---- + + /// + /// Creates a sorted set with a builder that an action fills. + /// + /// The action that adds the elements to the builder, which orders with the comparer. + /// The sorted set of the elements that the action added. + /// + /// O(1) apart from the cost of the action. The member wraps + /// + /// and . + /// + [] + member build : action : (SortedSet<'T>.Builder -> unit) -> SortedSet<'T> + + /// + /// Creates an empty builder that orders with the comparer. + /// + /// The builder. + /// + /// O(1). The member wraps + /// . + /// + [] + member builder : unit -> SortedSet<'T>.Builder + + // ---- Properties ---- + + /// + /// The comparer that orders the elements of the sorted sets that the struct creates. + /// + /// + /// O(1). The default value of the struct returns , as the + /// BCL does for a comparer, and not F# generic comparison: the struct serves element types without + /// F# comparison too. + /// + member Comparer : IComparer<'T> diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs new file mode 100644 index 00000000..a4f57a9e --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fs @@ -0,0 +1,399 @@ +[] +module FSharp.Collections.Immutable.SortedSet + +open System +open System.Collections.Generic +open System.Collections.Immutable +open System.Linq +// Opened after System.Collections.Generic, so that SortedSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +// The functions call the members of ImmutableSortedSet: Contains, TryGetValue, IndexOf, the indexer, Reverse, Add, +// Remove, Clear, the set algebra and the builders (ADR 0001, D5). The sorted sets created without a comparer compare with +// F# generic comparison, the comparer of the F# set (D5, Comparers); DefaultComparer resolves it once per type, so that +// the sorted sets of one type share it. The creation functions are the members of CustomComparerSortedSet, which add the +// elements to a builder: a builder keeps the first of the elements that compare equal as Set.ofSeq does, while +// CreateRange sorts unstably and can keep any of them. 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 when 'T : comparison> () = CustomComparerSortedSet<'T>(DefaultComparer<'T>.Instance) + +// ---- Creation ---- + +[] +[] +let empty<'T when 'T : comparison> : SortedSet<'T> = (withDefaultComparer<'T>()).empty + +[] +let forComparer (comparer : IComparer<'T>) = CustomComparerSortedSet<'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 : SortedSet<'T>.Builder -> unit) = (withDefaultComparer ()).build action + +[] +let builder () : SortedSet<'T>.Builder = (withDefaultComparer<'T>()).builder() + +[] +let ofBuilder (builder : SortedSet<'T>.Builder) = + checkNotNull (nameof builder) builder + builder.ToImmutable () + +[] +let toBuilder (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.ToBuilder () + +[] +let update (action : SortedSet<'T>.Builder -> unit) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + let builder = set.ToBuilder () + action builder + builder.ToImmutable () + +// ---- Properties ---- + +[] +let comparer (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.KeyComparer + +[] +let count (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Count + +[] +let isEmpty (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.IsEmpty + +// ---- Access ---- + +[] +let indexOf (value : 'T) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.IndexOf value + +[] +let item (index : int) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set[index] + +[] +let maxElement (set : SortedSet<'T>) = + checkNotNull (nameof set) set + + if set.IsEmpty then + invalidArg (nameof set) ErrorStrings.SetContainsNoElements + + // The indexer returns the element type. Max returns the default value for an empty sorted set, so it is nullable, + // and Unchecked.nonNull would constrain the element type to reference types. + set[set.Count - 1] + +[] +let minElement (set : SortedSet<'T>) = + checkNotNull (nameof set) set + + if set.IsEmpty then + invalidArg (nameof set) ErrorStrings.SetContainsNoElements + + // The indexer returns the element type. Min returns the default value for an empty sorted set, so it is nullable, + // and Unchecked.nonNull would constrain the element type to reference types. + set[0] + +[] +let tryGetValue (value : 'T) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + + match set.TryGetValue value with + | true, actual -> ValueSome actual + | false, _ -> ValueNone + +[] +let tryItem (index : int) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + + if index >= 0 && index < set.Count then + ValueSome set[index] + else + ValueNone + +// ---- Search ---- + +[] +let contains (element : 'T) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Contains element + +// exists and forall loop over the struct enumerator of the sorted set, which allocates nothing. LINQ Any and All allocated +// 152 bytes per call, a delegate and the boxed enumerator, and Seq.exists and Seq.forall 64 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 : SortedSet<'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 : SortedSet<'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 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsProperSubsetOf set2 + +[] +let isProperSuperset (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + + // ImmutableSortedSet.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 orders 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 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSubsetOf set2 + +[] +let isSuperset (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.IsSupersetOf set2 + +[] +let overlaps (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Overlaps set2 + +[] +let setEquals (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SetEquals set2 + +// ---- Transformation ---- + +[] +let add (value : 'T) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Add value + +// Union is not used: when the sequence is large against the set, it refills the tree from an unstable sort and can keep an +// element of the sequence in place of the equal element of the set (measured: 4 of 25 elements of the set kept for 250 +// added elements). The builder adds the elements one by one, so the element of the set stays and the first of several +// equal new elements is added in every case (ADR 0001, D14). +[] +let addRange (elements : seq<'T>) (set : SortedSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + let builder = set.ToBuilder () + let mutable added = false + + for item in elements do + if builder.Add item then + added <- true + + // The builder returns the input from ToImmutable when nothing changed, but the BCL does not document it; the flag makes + // the promise of the function independent of that + if added then builder.ToImmutable () else set + +[] +let choose (chooser : 'T -> 'U voption) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + SortedSets.choose chooser set + +[] +let clear (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Clear () + +[] +let difference (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Except set2 + +[] +let filter (predicate : 'T -> bool) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + let builder = ImmutableSortedSet.CreateBuilder<'T> set.KeyComparer + + for item in set do + if predicate item then + builder.Add item |> ignore + + builder.ToImmutable () + +[] +let intersect (set1 : SortedSet<'T>) (set2 : SortedSet<'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 : SortedSet<'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 : SortedSet<'T>) = + checkNotNull (nameof set) set + SortedSets.map mapping set + +[] +let remove (value : 'T) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Remove value + +[] +let removeRange (elements : seq<'T>) (set : SortedSet<'T>) = + checkNotNullOrDefaultSeq (nameof elements) elements + checkNotNull (nameof set) set + // ImmutableSortedSet returns 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 rev (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set.Reverse () + +[] +let symmetricDifference (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.SymmetricExcept set2 + +[] +let union (set1 : SortedSet<'T>) (set2 : SortedSet<'T>) = + checkNotNull (nameof set1) set1 + checkNotNull (nameof set2) set2 + set1.Union set2 + +[] +let unionMany (sets : seq>) = + checkNotNullOrDefaultSeq (nameof sets) sets + + let checkSet (set : SortedSet<'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 : SortedSet<'T>) = + checkNotNull (nameof set) set + Seq.fold folder state set + +[] +let foldBack (folder : 'T -> 'State -> 'State) (set : SortedSet<'T>) (state : 'State) = + checkNotNull (nameof set) set + // Reverse walks the tree from the largest element, so the elements are not copied into an array first + Seq.fold (fun state item -> folder item state) state (set.Reverse ()) + +[] +let iter (action : 'T -> unit) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + Seq.iter action set + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : SortedSet<'T>) = + checkNotNull (nameof set) set + let accepted = ImmutableSortedSet.CreateBuilder<'T> set.KeyComparer + let rejected = ImmutableSortedSet.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 : SortedSet<'T>) = + checkNotNull (nameof set) set + SortedSets.partitionWith partitioner set + +// ---- Conversion ---- + +[] +let toArray (set : SortedSet<'T>) = + checkNotNull (nameof set) set + Enumerable.ToArray set + +[] +let toList (set : SortedSet<'T>) = + checkNotNull (nameof set) set + List.ofSeq set + +[] +let toSeq (set : SortedSet<'T>) = + checkNotNull (nameof set) set + set :> seq<'T> diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fsi b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fsi new file mode 100644 index 00000000..0067de81 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSet.fsi @@ -0,0 +1,879 @@ +/// +/// Functions for , the immutable sorted 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 sorted set. +/// +/// +/// The creation functions create sorted sets that compare with F# generic comparison. Such a sorted set enumerates its +/// elements in the order of the F# set of the same elements: strings, for example, compare ordinally and not in the order +/// of the current culture. +/// +/// creates a , whose members create sorted sets +/// with another comparer (ADR 0001, D12). +/// +/// +/// The functions that visit the elements visit them in ascending order of the comparer of the sorted set; +/// +/// and +/// +/// visit them in descending order, as the functions of the same name in +/// do. +/// +/// +/// 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 F# generic comparison. +/// +/// +/// 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.SortedSet + +open System +open System.Collections.Generic +open System.Collections.Immutable +// Opened after System.Collections.Generic, so that SortedSet names the abbreviation of this library, not the mutable set +open FSharp.Collections.Immutable + +// ---- Creation ---- + +/// +/// The empty sorted set, which compares with F# generic comparison. +/// +/// +/// O(1). The value is with the comparer +/// . +/// +[] +[] +val empty<'T when 'T : comparison> : SortedSet<'T> + +/// +/// Creates the struct that creates sorted sets that order their elements with a comparer. +/// +/// The comparer of the elements. +/// +/// The whose members are the creation functions of +/// this module with the comparer: they create sorted sets and builders that order with it. +/// +/// Thrown when is . +/// +/// +/// O(1). Creating the struct allocates nothing. The element type needs no F# comparison, because the comparer orders the +/// elements. +/// +/// also gives an existing sorted set the comparer (ADR 0001, D12). +/// +/// +/// A comparer throws, where the BCL members Create, CreateRange, +/// CreateBuilder and WithComparer take it as +/// . For that meaning use the default value of the struct, +/// which orders with . The function +/// gives a +/// sorted set F# generic comparison instead, the default comparer of this module. +/// +/// +/// +/// +/// let names = (SortedSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "bob"; "Ann"; "ANN" ] +/// +/// Evaluates to a sorted set of "Ann" and "bob", in this order, that compares without case. +/// +[] +val forComparer : comparer : IComparer<'T> -> CustomComparerSortedSet<'T> + +/// +/// Creates a sorted set from the elements of an array. +/// +/// The input array. +/// The sorted set of the distinct elements of the array, which compares with F# generic comparison. +/// Thrown when is . +/// +/// O(n log n). Of the elements that compare equal, the sorted set keeps the first one, as +/// does. Later changes to the array do not change +/// the sorted set. +/// +[] +val ofArray : array : 'T array -> SortedSet<'T> when 'T : comparison + +/// +/// Creates a sorted set from the elements of a list. +/// +/// The input list. +/// The sorted set of the distinct elements of the list, which compares with F# generic comparison. +/// Thrown when is . +/// +/// O(n log n). Of the elements that compare equal, the sorted set keeps the first one, as +/// does. +/// +[] +val ofList : elements : 'T list -> SortedSet<'T> when 'T : comparison + +/// +/// Creates a sorted set from the elements of a read-only span. +/// +/// The input span. +/// +/// The sorted set of the distinct elements of the span, which compares with F# generic comparison; an empty span gives the +/// empty sorted set. +/// +/// +/// +/// O(n log n). Of the elements that compare equal, the sorted set keeps the first one, as +/// does. Later changes to the memory under the +/// span do not change the sorted set. +/// +/// +/// The function adds the elements one by one to a builder. It does not wrap +/// , which sorts the +/// elements with an unstable sort and can keep any of the elements that compare equal. +/// +/// +[] +val ofReadOnlySpan : span : ReadOnlySpan<'T> -> SortedSet<'T> when 'T : comparison + +/// +/// Creates a sorted set from the elements of a sequence. +/// +/// The input sequence. +/// The sorted set of the distinct elements of the sequence, which compares with F# generic comparison. +/// Thrown when is or a default flat list. +/// +/// O(n log n), or O(1) for a sorted set that has the comparer instance which this module uses for the element type: the +/// function returns such a sorted set without a copy, and gives a sorted set with another comparer F# generic comparison. +/// Of the elements that compare equal, the sorted set keeps the first one, as +/// does. +/// +[] +val ofSeq : elements : seq<'T> -> SortedSet<'T> when 'T : comparison + +/// +/// Creates a sorted set from the elements of a span. +/// +/// The input span. +/// +/// The sorted set of the distinct elements of the span, which compares with F# generic comparison; an empty span gives the +/// empty sorted set. +/// +/// +/// O(n log n). The function converts the span to a read-only span and does what +/// does: of the +/// elements that compare equal, the sorted set keeps the first one. +/// +[] +val ofSpan : span : Span<'T> -> SortedSet<'T> when 'T : comparison + +/// Creates a sorted set of one element. +/// The element. +/// The sorted set of one element, which compares with F# generic comparison. +/// O(1). +[] +val singleton : value : 'T -> SortedSet<'T> when 'T : comparison + +// ---- Builder ---- + +/// +/// Creates a sorted set with a builder that an action fills. +/// +/// +/// The action that adds the elements to the builder, which compares with F# generic comparison. +/// +/// The sorted set of the elements that the action added. +/// +/// O(1) apart from the cost of the action. The function wraps +/// +/// and . +/// +[] +val build : action : (SortedSet<'T>.Builder -> unit) -> SortedSet<'T> when 'T : comparison + +/// +/// Creates an empty builder that compares with F# generic comparison. +/// +/// The builder. +/// +/// O(1). The function wraps +/// +/// with the comparer . +/// +[] +val builder : unit -> SortedSet<'T>.Builder when 'T : comparison + +/// +/// Returns the contents of a builder as a sorted set. +/// +/// The builder, which stays usable. +/// The sorted 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 sorted set: the sorted set shares +/// the nodes of the builder instead of copying the elements. The function wraps +/// . +/// +[] +val ofBuilder : builder : SortedSet<'T>.Builder -> SortedSet<'T> + +/// +/// Creates a builder that contains the elements of a sorted set. +/// +/// The input sorted set, which stays unchanged. +/// The builder, with the comparer of the sorted set. +/// Thrown when is . +/// +/// O(1). The function wraps . The builder +/// shares the nodes of the sorted set until it changes them. +/// +[] +val toBuilder : set : SortedSet<'T> -> SortedSet<'T>.Builder + +/// +/// Creates a sorted set from a builder over the elements of a sorted set, after an action changes the builder. +/// +/// The action that changes the builder, which has the comparer of the sorted set. +/// The input sorted set, which stays unchanged. +/// The sorted 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 +/// . It makes the in-place +/// operations of the builder, such as IntersectWith and SymmetricExceptWith, one expression. +/// +[] +val update : action : (SortedSet<'T>.Builder -> unit) -> set : SortedSet<'T> -> SortedSet<'T> + +// ---- Properties ---- + +/// +/// Returns the comparer that orders the elements of the sorted set. +/// +/// The input sorted set. +/// The comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val comparer : set : SortedSet<'T> -> IComparer<'T> + +/// Returns the number of elements of the sorted set. +/// The input sorted set. +/// The number of elements. +/// Thrown when is . +/// O(1). +[] +val count : set : SortedSet<'T> -> int + +/// Returns whether the sorted set has no elements. +/// The input sorted set. +/// when the sorted set is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : set : SortedSet<'T> -> bool + +// ---- Access ---- + +/// +/// Returns the index of the element equal to a value, in the comparer of the sorted set. +/// +/// The value to find. +/// The input sorted set. +/// +/// The index of the equal element in ascending order. When no element equals the value, the bitwise complement of the +/// index of the first larger element, or the bitwise complement of the number of elements when no element is larger. +/// +/// Thrown when is . +/// +/// O(log n). The function wraps . A missing +/// value gives a negative result, whose bitwise complement is the index at which the value would be inserted. +/// +[] +val indexOf : value : 'T -> set : SortedSet<'T> -> int + +/// +/// Returns the element at an index, in ascending order. +/// +/// The index of the element. +/// The input sorted set. +/// The element at the index. +/// Thrown when is . +/// Thrown when the index is negative or not less than the number of elements. +/// +/// O(log n). The function wraps the indexer +/// and throws what it throws: +/// has no function to mirror. +/// +[] +val item : index : int -> set : SortedSet<'T> -> 'T + +/// +/// Returns the largest element, in the order of the comparer of the sorted set. +/// +/// The input sorted set. +/// The last element in ascending order. +/// Thrown when is . +/// Thrown when the sorted set is empty. +/// +/// O(log n). The function throws for an empty sorted set, as +/// +/// does, and reads the last element with the indexer +/// . +/// would return the default value of the element +/// type for an empty sorted set. +/// +[] +val maxElement : set : SortedSet<'T> -> 'T + +/// +/// Returns the smallest element, in the order of the comparer of the sorted set. +/// +/// The input sorted set. +/// The first element in ascending order. +/// Thrown when is . +/// Thrown when the sorted set is empty. +/// +/// O(log n). The function throws for an empty sorted set, as +/// +/// does, and reads the first element with the indexer +/// . +/// would return the default value of the element +/// type for an empty sorted set. +/// +[] +val minElement : set : SortedSet<'T> -> 'T + +/// +/// Returns the element of the sorted set that equals a value, or ValueNone when there is none. +/// +/// The value to search for. +/// The input sorted set. +/// The element that the comparer of the sorted 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 sorted set holds. +/// +[] +val tryGetValue : value : 'T -> set : SortedSet<'T> -> 'T voption + +/// +/// Returns the element at an index, in ascending order, or ValueNone when the index is out of range. +/// +/// The index of the element. +/// The input sorted set. +/// The element at the index, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function checks the index against the number of elements and wraps the indexer +/// . +/// +[] +val tryItem : index : int -> set : SortedSet<'T> -> 'T voption + +// ---- Search ---- + +/// +/// Returns whether the sorted set contains an element equal to a value, in the comparer of the sorted set. +/// +/// The value to find. +/// The input sorted set. +/// when an element equals the value. +/// Thrown when is . +/// +/// O(log n). +/// +[] +val contains : element : 'T -> set : SortedSet<'T> -> bool + +/// +/// Returns whether any element satisfies the predicate. +/// +/// The function to test the elements. +/// The input sorted set. +/// when an element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function tests the elements in ascending order and stops at the first one that satisfies the predicate. +/// +[] +val exists : predicate : ('T -> bool) -> set : SortedSet<'T> -> bool + +/// +/// Returns whether all elements satisfy the predicate. +/// +/// The function to test the elements. +/// The input sorted set. +/// when every element satisfies the predicate. +/// Thrown when is . +/// +/// O(n). The function tests the elements in ascending order and stops at the first one that does not satisfy the +/// predicate. +/// +[] +val forall : predicate : ('T -> bool) -> set : SortedSet<'T> -> bool + +/// +/// Returns whether every element of the first sorted 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 sorted set is a proper subset of the second one. +/// Thrown when or is . +/// +/// O(m log (n + m)), where n and m are the numbers of elements of the first and the second sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val isProperSubset : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +/// +/// Returns whether every element of the second sorted 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 sorted 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 sorted set, and O(m log m) more for a +/// helper set when the sorted sets have different comparers. +/// +/// +/// The function compares in the comparer of the first sorted set, with set semantics: elements of the second sorted 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 sorted sets have the same +/// comparer, and otherwise answers with +/// +/// and . +/// +/// +[] +val isProperSuperset : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +/// +/// Returns whether every element of the first sorted set is in the second one. +/// +/// The potential subset. +/// The set to test against. +/// when the first sorted set is a subset of the second one. +/// Thrown when or is . +/// +/// O(m log (n + m)), where n and m are the numbers of elements of the first and the second sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val isSubset : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +/// +/// Returns whether every element of the second sorted set is in the first one. +/// +/// The potential superset. +/// The set to test against. +/// when the first sorted 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 sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val isSuperset : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +/// +/// Returns whether the sorted sets have an element in common. +/// +/// The first sorted set. +/// The second sorted set. +/// when an element of the second sorted 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 sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val overlaps : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +/// +/// Returns whether the sorted sets have the same elements. +/// +/// The first sorted set. +/// The second sorted set. +/// when every element of each sorted set is in the other one. +/// Thrown when or is . +/// +/// O(m log (n + m)), where n and m are the numbers of elements of the first and the second sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val setEquals : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> bool + +// ---- Transformation ---- + +/// Returns a sorted set with a value added. +/// The value to add. +/// The input sorted set. +/// +/// A sorted 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 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns a sorted set with the elements of a sequence added. +/// +/// The elements to add. +/// The input sorted set. +/// +/// A sorted 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 sorted set and the sequence. The function adds the +/// elements one by one to the builder of the sorted set. +/// +/// +/// An element of the sequence that the comparer of the sorted set considers equal to an element of the sorted set does not +/// replace it, and of several equal elements of the sequence the first one is added (ADR 0001, D14). +/// +/// +/// The function does not wrap +/// . +/// When the sequence is large against the sorted set, that member refills the tree from an unstable sort and can keep an +/// element of the sequence in place of the equal element of the sorted set. The price is time: for such a sequence the +/// BCL member is faster, and the more so the larger the sequence, because it sorts the elements and builds the tree at +/// once, while the function inserts them one by one. For a sequence that is not larger than the sorted set both do the +/// same work. To unite two sorted sets use +/// , +/// which keeps the element of the larger sorted set when both have the same comparer. +/// +/// +[] +val addRange : elements : seq<'T> -> set : SortedSet<'T> -> SortedSet<'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 sorted set. +/// The sorted set of the distinct chosen values, which compares with F# generic comparison. +/// Thrown when is . +/// +/// O(n log n). The function calls the chooser in ascending order of the elements and keeps the first of the values that +/// compare equal. 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 : SortedSet<'T> -> SortedSet<'U> when 'U : comparison + +/// +/// Returns an empty sorted set with the comparer of the input. +/// +/// The input sorted set. +/// The empty sorted set; the input itself when it is empty. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val clear : set : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements of the first sorted set that are not in the second one. +/// +/// The first sorted set. +/// The sorted set whose elements are removed from the first one. +/// The difference, with the comparer of the first sorted set. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +[] +val difference : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements that satisfy the predicate. +/// +/// The function to test the elements. +/// The input sorted set. +/// The sorted set of the elements that satisfy the predicate, with the comparer of the input. +/// Thrown when is . +/// +/// O(n log n). The function tests the elements in ascending order. +/// +[] +val filter : predicate : ('T -> bool) -> set : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements that are in both sorted sets. +/// +/// The first sorted set. +/// The second sorted set. +/// The intersection, with the comparer of the first sorted set. +/// Thrown when or is . +/// +/// O(m log n), where n and m are the numbers of elements of the first and the second sorted set. The function wraps +/// , +/// which compares in the comparer of the first sorted set and keeps the elements of the second sorted set: where the +/// comparer considers two different elements equal, the intersection holds the one of the second sorted set. +/// +[] +val intersect : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements that are in every sorted set of a sequence. +/// +/// The sorted sets. +/// The intersection, with the comparer of the first sorted set. +/// Thrown when is or a default flat list, or one of its sorted sets is . +/// Thrown when the sequence is empty. +/// +/// O(N log n), where N is the total number of elements of the other sorted sets and n the number of elements of the +/// first one. The function intersects the sorted sets from the first one to the last one. +/// +[] +val intersectMany : sets : seq> -> SortedSet<'T> + +/// +/// Returns the results of a function applied to each element. +/// +/// The function to transform the elements. +/// The input sorted set. +/// The sorted set of the distinct results, which compares with F# generic comparison. +/// Thrown when is . +/// +/// O(n log n). The function maps the elements in ascending order and keeps the first of the results that compare equal, +/// as +/// does. 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 : SortedSet<'T> -> SortedSet<'U> when 'U : comparison + +/// Returns a sorted set without an element equal to a value. +/// The value to remove. +/// The input sorted set. +/// +/// A sorted 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 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns a sorted set without the elements that are equal to an element of a sequence. +/// +/// The elements to remove; an element without an equal in the sorted set is skipped. +/// The input sorted set. +/// +/// A sorted 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 sorted set and the sequence. The function wraps +/// +/// and compares in the comparer of the sorted set. +/// +[] +val removeRange : elements : seq<'T> -> set : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements in descending order. +/// +/// The input sorted set. +/// The sequence of the elements, from the largest one to the smallest one. +/// Thrown when is . +/// +/// O(1), and O(n) to enumerate the sequence. The function wraps +/// , whose sequence walks the sorted set from +/// the end without a copy. +/// +[] +val rev : set : SortedSet<'T> -> seq<'T> + +/// +/// Returns the elements that are in exactly one of the sorted sets. +/// +/// The first sorted set. +/// The second sorted set. +/// The symmetric difference, with the comparer of the first sorted 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 sorted set. The function +/// wraps +/// and compares in the comparer of the first sorted set. +/// +[] +val symmetricDifference : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements that are in either sorted set. +/// +/// The first sorted set. +/// The second sorted set. +/// The union, with the comparer of the first sorted 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 sorted set. The function wraps +/// +/// and compares in the comparer of the first sorted set. +/// +/// +/// Where the comparer considers two different elements equal, the union holds the element of the first sorted set, or the +/// element of the larger one when both have the same comparer, because the BCL member then adds the smaller sorted set to +/// the larger one (ADR 0001, D14). When the second sorted set has another comparer and is much larger than the first one, +/// the BCL member refills the tree from an unstable sort, and either element can stay. To add the elements of a sequence +/// to a sorted set and keep the elements of the sorted set in every case, use +/// . +/// +/// +[] +val union : set1 : SortedSet<'T> -> set2 : SortedSet<'T> -> SortedSet<'T> + +/// +/// Returns the elements that are in any sorted set of a sequence. +/// +/// The sorted sets. +/// +/// The union, with the comparer of the first sorted set; the empty sorted set, which compares with F# generic comparison, +/// for an empty sequence. +/// +/// Thrown when is or a default flat list, or one of its sorted sets is . +/// +/// O(N log N), where N is the total number of elements of the sorted sets. The function unites the sorted sets from the +/// first one to the last one. +/// +[] +val unionMany : sets : seq> -> SortedSet<'T> when 'T : comparison + +// ---- Folding ---- + +/// +/// Applies a function to a state and each element, in ascending order. +/// +/// The function that computes the next state. +/// The initial state. +/// The input sorted set. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'T -> 'State) -> state : 'State -> set : SortedSet<'T> -> 'State + +/// +/// Applies a function to each element and a state, in descending order. +/// +/// The function that computes the next state. +/// The input sorted set. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function visits the elements from the largest one, as +/// +/// does. It enumerates and copies no element. +/// +[] +val foldBack : folder : ('T -> 'State -> 'State) -> set : SortedSet<'T> -> state : 'State -> 'State + +/// Applies an action to each element, in ascending order. +/// The action. +/// The input sorted set. +/// Thrown when is . +/// O(n). +[] +val iter : action : ('T -> unit) -> set : SortedSet<'T> -> unit + +// ---- Partitioning ---- + +/// +/// Splits the elements by a predicate. +/// +/// The function to test the elements. +/// The input sorted set. +/// +/// The sorted set of the elements that satisfy the predicate and the sorted set of the other ones, both with the comparer +/// of the input. +/// +/// Thrown when is . +/// +/// O(n log n). The function tests the elements in ascending order. +/// +[] +val partition : predicate : ('T -> bool) -> set : SortedSet<'T> -> struct (SortedSet<'T> * SortedSet<'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 sorted set. +/// +/// The sorted set of the values of the first choices and the sorted set of the values of the second choices, which +/// compare with F# generic comparison. +/// +/// Thrown when is . +/// +/// O(n log n). The function calls the partitioner in descending order of the elements and keeps the first of the values +/// that compare equal, as +/// does, so that the same one of two equal values stays. +/// +[] +val partitionWith : + partitioner : ('T -> Choice<'T1, 'T2>) -> set : SortedSet<'T> -> struct (SortedSet<'T1> * SortedSet<'T2>) + when 'T1 : comparison and 'T2 : comparison + +// ---- Conversion ---- + +/// Returns a new array with the elements, in ascending order. +/// The input sorted set. +/// A copy of the elements. +/// Thrown when is . +/// O(n). +[] +val toArray : set : SortedSet<'T> -> 'T array + +/// Returns a list of the elements, in ascending order. +/// The input sorted set. +/// The list of the elements. +/// Thrown when is . +/// O(n). +[] +val toList : set : SortedSet<'T> -> 'T list + +/// Returns the sorted set as a sequence, in ascending order. +/// The input sorted set. +/// The sequence of the elements. +/// Thrown when is . +/// O(1). +[] +val toSeq : set : SortedSet<'T> -> seq<'T> diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fs new file mode 100644 index 00000000..5ca58f03 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fs @@ -0,0 +1,91 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Immutable + +// System.Collections.Generic is not opened: its SortedSet would shadow the SortedSet abbreviation of this namespace. + +type SortedSetBuffer<'T> = SortedSet<'T>.Builder -> unit + +[] +type SortedSetCE<'T when 'T : comparison> (comparer : System.Collections.Generic.IComparer<'T>) = + + // The builder checks its comparer when it is created, as CustomComparerSortedSet does, not when an expression runs + do checkNotNull (nameof comparer) comparer + + new () = SortedSetCE<'T>(DefaultComparer<'T>.Instance) + + member inline _.Zero () : SortedSetBuffer<'T> = ignore + + member inline _.Yield (item : 'T) : SortedSetBuffer<'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>) : SortedSetBuffer<'T> = + checkNotNullOrDefaultSeq (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (items : 'T array) : SortedSetBuffer<'T> = + checkNotNull (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (items : SortedSet<'T>) : SortedSetBuffer<'T> = + checkNotNull (nameof items) items + fun builder -> builder.UnionWith items + + member inline _.YieldFrom (buffer : SortedSetBuffer<'T>) : SortedSetBuffer<'T> = buffer + + member inline _.Combine (first : SortedSetBuffer<'T>, second : SortedSetBuffer<'T>) : SortedSetBuffer<'T> = + fun builder -> + first builder + second builder + + member inline _.Delay ([] delayed : unit -> SortedSetBuffer<'T>) : SortedSetBuffer<'T> = + fun builder -> delayed () builder + + member inline _.For (source : seq<'U>, [] body : 'U -> SortedSetBuffer<'T>) : SortedSetBuffer<'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 : SortedSetBuffer<'T>) : SortedSetBuffer<'T> = + fun builder -> + while guard () do + body builder + + member inline _.TryWith + (body : SortedSetBuffer<'T>, [] handler : exn -> SortedSetBuffer<'T>) + : SortedSetBuffer<'T> + = + fun builder -> + try + body builder + with ex -> + handler ex builder + + member inline _.TryFinally + (body : SortedSetBuffer<'T>, [] compensation : unit -> unit) + : SortedSetBuffer<'T> + = + fun builder -> + try + body builder + finally + compensation () + + member inline _.Using + (resource : 'Resource, [] body : 'Resource -> SortedSetBuffer<'T>) + : SortedSetBuffer<'T> + = + fun builder -> + use resource = resource + body resource builder + + member _.Run (buffer : SortedSetBuffer<'T>) : SortedSet<'T> = + let builder = ImmutableSortedSet.CreateBuilder<'T> comparer + buffer builder + builder.ToImmutable () diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fsi b/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fsi new file mode 100644 index 00000000..b5dceabb --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetCE.fsi @@ -0,0 +1,93 @@ +namespace FSharp.Collections.Immutable + +open System + +// System.Collections.Generic is not opened: its SortedSet would shadow the SortedSet abbreviation of this namespace. + +/// +/// The body of a sortedSet { } expression: a function that adds the elements to a builder. +/// +type SortedSetBuffer<'T> = SortedSet<'T>.Builder -> unit + +/// +/// The builder of the sortedSet { } computation expression. +/// +/// +/// +/// The expression collects its elements in an +/// with the comparer of the builder (ADR 0001, D7). An element equal to an earlier one is not added again, so the first of +/// the equal elements stays. sortedSet { } compares with F# generic comparison, +/// ; an instance created with a comparer +/// builds sorted sets that order their elements with it. +/// +/// +/// The element type must support F# comparison even when the instance is created with a comparer: the constructor without +/// a comparer needs it, and a constraint holds for every constructor of a class. +/// +/// +[] +type SortedSetCE<'T when 'T : comparison> = + + /// + /// Creates the builder of sorted sets that order their elements with a comparer. + /// + /// The comparer of the elements of the sorted set. + /// Thrown when is . + new : comparer : System.Collections.Generic.IComparer<'T> -> SortedSetCE<'T> + + /// + /// Creates the builder of sorted sets that compare with F# generic comparison. + /// + new : unit -> SortedSetCE<'T> + + /// An expression without elements. + member inline Zero : unit -> SortedSetBuffer<'T> + + /// Adds an element. + member inline Yield : item : 'T -> SortedSetBuffer<'T> + + /// Adds the elements of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline YieldFrom : items : seq<'T> -> SortedSetBuffer<'T> + + /// Adds the elements of an array. + /// Thrown when the array is . + member inline YieldFrom : items : 'T array -> SortedSetBuffer<'T> + + /// Adds the elements of a sorted set. + /// Thrown when the sorted set is . + member inline YieldFrom : items : SortedSet<'T> -> SortedSetBuffer<'T> + + /// Adds the elements of another expression body. + member inline YieldFrom : buffer : SortedSetBuffer<'T> -> SortedSetBuffer<'T> + + /// Adds the elements of two parts of the expression, in order. + member inline Combine : first : SortedSetBuffer<'T> * second : SortedSetBuffer<'T> -> SortedSetBuffer<'T> + + /// + /// Runs the body of the expression each time the elements are collected. + /// + member inline Delay : [] delayed : (unit -> SortedSetBuffer<'T>) -> SortedSetBuffer<'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 -> SortedSetBuffer<'T>) -> SortedSetBuffer<'T> + + /// Adds the elements of the body while the guard holds. + member inline While : [] guard : (unit -> bool) * body : SortedSetBuffer<'T> -> SortedSetBuffer<'T> + + /// Handles the exceptions of the body. + member inline TryWith : + body : SortedSetBuffer<'T> * [] handler : (exn -> SortedSetBuffer<'T>) -> SortedSetBuffer<'T> + + /// Runs the compensation after the body. + member inline TryFinally : + body : SortedSetBuffer<'T> * [] compensation : (unit -> unit) -> SortedSetBuffer<'T> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> SortedSetBuffer<'T>) -> SortedSetBuffer<'T> + when 'Resource :> IDisposable | null + + /// Collects the elements of the expression. + member Run : buffer : SortedSetBuffer<'T> -> SortedSet<'T> diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs new file mode 100644 index 00000000..8243f4bd --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs @@ -0,0 +1,24 @@ +[] +module FSharp.Collections.Immutable.Option.SortedSet + +open FSharp.Collections.Immutable + +// The functions delegate to the default module and convert between options and value options. + +// ---- Access ---- + +[] +let tryGetValue (value : 'T) (set : SortedSet<'T>) = + FSharp.Collections.Immutable.SortedSet.tryGetValue value set + |> ValueOption.toOption + +[] +let tryItem (index : int) (set : SortedSet<'T>) = + FSharp.Collections.Immutable.SortedSet.tryItem index set + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let choose (chooser : 'T -> 'U option) (set : SortedSet<'T>) = + FSharp.Collections.Immutable.SortedSet.choose (fun item -> chooser item |> ValueOption.ofOption) set diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fsi b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fsi new file mode 100644 index 00000000..408ea5b6 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fsi @@ -0,0 +1,61 @@ +/// +/// 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 none of +/// these functions; they take and return options as FSharp.Core would. +/// +[] +module FSharp.Collections.Immutable.Option.SortedSet + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the element of the sorted set that equals a value, or None when there is none. +/// +/// The value to search for. +/// The input sorted set. +/// The element that the comparer of the sorted 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 sorted set holds. +/// +[] +val tryGetValue : value : 'T -> set : SortedSet<'T> -> 'T option + +/// +/// Returns the element at an index, in ascending order, or None when the index is out of range. +/// +/// The index of the element. +/// The input sorted set. +/// The element at the index, or None. +/// Thrown when is . +/// +/// O(log n). The function checks the index against the number of elements and wraps the indexer +/// . +/// +[] +val tryItem : index : int -> set : SortedSet<'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 sorted set. +/// The sorted set of the distinct chosen values, which compares with F# generic comparison. +/// Thrown when is . +/// +/// O(n log n). The function calls the chooser in ascending order of the elements and keeps the first of the values that +/// compare equal. 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 : SortedSet<'T> -> SortedSet<'U> when 'U : comparison diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs new file mode 100644 index 00000000..75cc7cad --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs @@ -0,0 +1,18 @@ +[] +module FSharp.Collections.Immutable.Tuple.SortedSet + +open FSharp.Collections.Immutable + +// The functions delegate to the default module and repack its struct tuples. + +// ---- Partitioning ---- + +[] +let partition (predicate : 'T -> bool) (set : SortedSet<'T>) = + let struct (accepted, rejected) = FSharp.Collections.Immutable.SortedSet.partition predicate set + accepted, rejected + +[] +let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : SortedSet<'T>) = + let struct (first, second) = FSharp.Collections.Immutable.SortedSet.partitionWith partitioner set + first, second diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fsi b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fsi new file mode 100644 index 00000000..f768a891 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fsi @@ -0,0 +1,51 @@ +/// +/// 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.SortedSet + +open FSharp.Collections.Immutable + +// ---- Partitioning ---- + +/// +/// Splits the elements by a predicate. +/// +/// The function to test the elements. +/// The input sorted set. +/// +/// The sorted set of the elements that satisfy the predicate and the sorted set of the other ones, both with the comparer +/// of the input. +/// +/// Thrown when is . +/// +/// O(n log n). The function tests the elements in ascending order. +/// +[] +val partition : predicate : ('T -> bool) -> set : SortedSet<'T> -> SortedSet<'T> * SortedSet<'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 sorted set. +/// +/// The sorted set of the values of the first choices and the sorted set of the values of the second choices, which +/// compare with F# generic comparison. +/// +/// Thrown when is . +/// +/// O(n log n). The function calls the partitioner in descending order of the elements and keeps the first of the values +/// that compare equal, as +/// does, so that the same one of two equal values stays. +/// +[] +val partitionWith : + partitioner : ('T -> Choice<'T1, 'T2>) -> set : SortedSet<'T> -> SortedSet<'T1> * SortedSet<'T2> + when 'T1 : comparison and 'T2 : comparison diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetTypes.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTypes.fs new file mode 100644 index 00000000..651841fa --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTypes.fs @@ -0,0 +1,17 @@ +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 sorted set, . +/// +/// +/// A sorted set orders its elements with a comparer and enumerates them in ascending order. The comparer is F# generic +/// comparison, , as for +/// , unless a +/// created the sorted set with another one. +/// +type SortedSet<'T> = ImmutableSortedSet<'T> From dd39d1b343625e39074ad989d4bc5a0c4541de56 Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:56:40 +0200 Subject: [PATCH 2/4] test: `SortedSet` against the set contracts and the order of `Set` - `SortedSet` derives a test class from each of the four set facets. Its ops records read every sorted set through a `toList` that checks the order of `Set`, so the contracts, which compare the elements of a hash set as a sorted list, check the order of a sorted set too without a change. `Set.minElement` names its set `s`, which the ops map to `set`. - Its own tests cover the order of strings, negative numbers and tuples through every creation function, `foldBack` and `rev` in descending order, the comparers that the functions keep or set, one comparer per element type, the first of the equal elements that the creation functions keep, the sorted-only functions `item`, `tryItem`, `indexOf`, `minElement` and `maxElement` with the exceptions of `Set` and of the BCL, shared storage, null inputs, the builder functions, `sortedSet { }` and the `Option` and `Tuple` namespaces. - `ISet.filter` keeps the implementation and the comparer of a sorted set. Co-Authored-By: Claude Opus 5.5 --- .../FSharp.Collections.Immutable.Tests.fsproj | 7 + .../HashSet/ISetTests.fs | 17 + .../SortedSet/SortedSetBuilderTests.fs | 207 +++ .../SortedSetComputationExpressionTests.fs | 278 ++++ .../SortedSet/SortedSetContractTests.fs | 26 + .../SortedSet/SortedSetOps.fs | 144 ++ .../SortedSet/SortedSetOptionTests.fs | 73 ++ .../SortedSet/SortedSetTests.fs | 1153 +++++++++++++++++ .../SortedSet/SortedSetTupleTests.fs | 61 + 9 files changed, 1966 insertions(+) create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetBuilderTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetComputationExpressionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetContractTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOps.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOptionTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs create mode 100644 tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTupleTests.fs 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 acc60db8..20fbae05 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -82,6 +82,13 @@ + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs index ec4e457b..756a589b 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashSet/ISetTests.fs @@ -284,6 +284,23 @@ type ISetTests () = $"ISet.%s{name} must keep the comparer" ) + [] + member _.``filter keeps the implementation and the comparer of a sorted set`` () = + let descending = ComparisonIdentity.FromFunction (fun (left : int) right -> compare right left) + let set = (SortedSet.forComparer descending).ofArray Inputs.Several :> IImmutableSet + let filtered = ISet.filter (fun item -> item > 0) set + + let sorted = + Assert.IsInstanceOfType>(filtered, "ISet.filter must keep the sorted set of its input") + + Assert.AreSame (descending, SortedSet.comparer sorted, "ISet.filter must keep the comparer of the sorted set") + + CollectionAssert.AreEqual ( + [| 12; 8; 7; 5 |], + SortedSet.toArray sorted, + "ISet.filter must keep the order of the comparer of the sorted set" + ) + [] 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 diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetBuilderTests.fs new file mode 100644 index 00000000..b7f1b431 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetBuilderTests.fs @@ -0,0 +1,207 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The builder functions of `SortedSet` and the builder of its struct with a comparer; sorted sets have no builder module +/// (ADR 0001, Appendix C). +[] +type SortedSetBuilderTests () = + + static let ignoreCase : IComparer = StringComparer.OrdinalIgnoreCase + + /// Creates sorted sets and builders that order strings without case. + static let caseless = SortedSet.forComparer ignoreCase + + /// The comparer of the sorted sets of strings created without one. + static let defaultComparer : IComparer = LanguagePrimitives.FastGenericComparer + + [] + member _.``build returns the sorted set of the elements that the action added`` () = + let set = + SortedSet.build (fun builder -> + builder.Add "b" |> ignore + builder.Add "a" |> ignore + builder.Add "b" |> ignore + ) + + CollectionAssert.AreEqual ( + [| "a"; "b" |], + SortedSet.toArray set, + "SortedSet.build must hold the elements that the action added" + ) + + Assert.AreSame (defaultComparer, SortedSet.comparer set, "SortedSet.build must compare with F# generic comparison") + + [] + member _.``build hands an empty builder with F# generic comparison to the action`` () = + let set = + SortedSet.build(fun builder -> + Assert.IsEmpty (builder, "SortedSet.build must hand over an empty builder") + + Assert.AreSame ( + defaultComparer, + builder.KeyComparer, + "SortedSet.build must hand over a builder that compares with F# generic comparison" + ) + ) + + Assert.IsEmpty (set, "SortedSet.build of an action that adds nothing must return an empty sorted set") + + [] + member _.``the build of the struct orders with its comparer`` () = + let set = + caseless.build (fun builder -> + Assert.AreSame ( + ignoreCase, + builder.KeyComparer, + "CustomComparerSortedSet.build must hand over a builder that orders with its comparer" + ) + + builder.UnionWith [| "banana"; "apple"; "BANANA" |] + ) + + CollectionAssert.AreEqual ( + [| "apple"; "banana" |], + SortedSet.toArray set, + "CustomComparerSortedSet.build must keep the first of the elements that the comparer considers equal" + ) + + Assert.AreSame (ignoreCase, SortedSet.comparer set, "CustomComparerSortedSet.build must keep the comparer") + + [] + member _.``builder starts empty with F# generic comparison`` () = + let builder = SortedSet.builder() + Assert.IsEmpty (builder, "SortedSet.builder must start without elements") + + Assert.AreSame ( + LanguagePrimitives.FastGenericComparer, + builder.KeyComparer, + "SortedSet.builder must compare with F# generic comparison" + ) + + builder.UnionWith [| "b"; "B"; "a"; "A" |] + + CollectionAssert.AreEqual ( + [| "A"; "B"; "a"; "b" |], + SortedSet.toArray (SortedSet.ofBuilder builder), + "SortedSet.builder must order strings ordinally" + ) + + [] + member _.``the builder of the struct orders with its comparer`` () = + let builder = caseless.builder () + Assert.IsEmpty (builder, "CustomComparerSortedSet.builder must start without elements") + Assert.IsTrue (builder.Add "banana", "The builder must add the first element") + Assert.IsTrue (builder.Add "apple", "The builder must add the second element") + Assert.IsFalse (builder.Add "APPLE", "CustomComparerSortedSet.builder must compare without case") + Assert.AreSame (ignoreCase, builder.KeyComparer, "CustomComparerSortedSet.builder must keep the comparer") + + CollectionAssert.AreEqual ( + [| "apple"; "banana" |], + Seq.toArray builder, + "CustomComparerSortedSet.builder must order with the comparer" + ) + + [] + member _.``ofBuilder returns the contents and keeps the builder usable`` () = + let builder = caseless.builder () + builder.UnionWith [| "cherry"; "apple" |] + let set = SortedSet.ofBuilder builder + builder.Add "banana" |> ignore + + CollectionAssert.AreEqual ( + [| "apple"; "cherry" |], + SortedSet.toArray set, + "SortedSet.ofBuilder must hold the elements of the builder" + ) + + Assert.HasCount (3, builder, "SortedSet.ofBuilder must keep the builder usable") + Assert.AreSame (ignoreCase, SortedSet.comparer set, "SortedSet.ofBuilder must keep the comparer of the builder") + + [] + member _.``toBuilder holds the elements and the comparer of the sorted set`` () = + let set = caseless.ofArray [| "cherry"; "apple" |] + let builder = SortedSet.toBuilder set + builder.Add "banana" |> ignore + builder.Remove "APPLE" |> ignore + + Assert.AreSame (ignoreCase, builder.KeyComparer, "SortedSet.toBuilder must keep the comparer") + + CollectionAssert.AreEqual ( + [| "banana"; "cherry" |], + Seq.toArray builder, + "SortedSet.toBuilder must hold the elements in order" + ) + + CollectionAssert.AreEqual ( + [| "apple"; "cherry" |], + SortedSet.toArray set, + "SortedSet.toBuilder must not change the sorted set" + ) + + [] + member _.``update changes a copy of the sorted set`` () = + let set = caseless.ofArray [| "apple"; "Banana" |] + + let updated = + SortedSet.update + (fun builder -> + builder.Add "cherry" |> ignore + builder.Remove "APPLE" |> ignore + ) + set + + CollectionAssert.AreEqual ( + [| "Banana"; "cherry" |], + SortedSet.toArray updated, + "SortedSet.update must hold the changed elements" + ) + + CollectionAssert.AreEqual ( + [| "apple"; "Banana" |], + SortedSet.toArray set, + "SortedSet.update must not change the sorted set" + ) + + Assert.AreSame (ignoreCase, SortedSet.comparer updated, "SortedSet.update must keep the comparer") + + [] + member _.``update hands a builder with the elements and the comparer of the sorted set to the action`` () = + let set = caseless.ofArray [| "apple"; "Banana" |] + + SortedSet.update + (fun builder -> + CollectionAssert.AreEqual ( + [| "apple"; "Banana" |], + Seq.toArray builder, + "SortedSet.update must hand over a builder with the elements of the sorted set" + ) + + Assert.AreSame ( + ignoreCase, + builder.KeyComparer, + "SortedSet.update must hand over a builder with the comparer of the sorted 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 = SortedSet.ofArray [| 1; 2; 3 |] + + CollectionAssert.AreEqual ( + [| 2; 3 |], + SortedSet.toArray (SortedSet.update (fun builder -> builder.IntersectWith [| 2; 3; 9 |]) set), + "SortedSet.update must apply IntersectWith" + ) + + CollectionAssert.AreEqual ( + [| 1; 3; 9 |], + SortedSet.toArray (SortedSet.update (fun builder -> builder.SymmetricExceptWith [| 2; 9 |]) set), + "SortedSet.update must apply SymmetricExceptWith" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetComputationExpressionTests.fs new file mode 100644 index 00000000..a8bff68f --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetComputationExpressionTests.fs @@ -0,0 +1,278 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `sortedSet { }` computation expression and the builders with a comparer. +[] +type SortedSetComputationExpressionTests () = + + /// A flat list of strings as a sequence of IComparable. A flat list of strings is also a sequence of IComparable, 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 IComparable does not see it. obj has no F# comparison, which the expression requires, and IComparable has. + static let stringsAsComparables (strings : FlatList) : seq = + match box strings with + | :? seq as widened -> widened + | _ -> invalidOp "A flat list of strings must be a sequence of IComparable" + + /// + /// 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 + let widenedElements = stringsAsComparables Unchecked.defaultof> + + Map.ofArray [| + "yield! (null sequence)", struct ("items", (fun () -> sortedSet { yield! nullElements } |> ignore)) + "yield! (default flat list)", struct ("items", (fun () -> sortedSet { yield! defaultElements } |> ignore)) + "yield! (default flat list of strings as comparables)", + struct ("items", (fun () -> sortedSet { yield! widenedElements } |> ignore)) + "yield! (null array)", + struct ("items", + (fun () -> + sortedSet { yield! Unchecked.defaultof } + |> ignore + )) + "yield! (null sorted set)", + struct ("items", + (fun () -> + sortedSet { yield! Unchecked.defaultof> } + |> ignore + )) + "for (null sequence)", + struct ("source", + (fun () -> + sortedSet { + for item in nullElements do + yield item + } + |> ignore + )) + "for (default flat list)", + struct ("source", + (fun () -> + sortedSet { + for item in defaultElements do + yield item + } + |> ignore + )) + "for (default flat list of strings as comparables)", + struct ("source", + (fun () -> + sortedSet { + 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 : SortedSet = sortedSet { () } + Assert.IsTrue (SortedSet.isEmpty set, "sortedSet { () } must be empty") + + [] + member _.``yield adds each distinct element once in ascending order`` () = + let set = sortedSet { + yield 2 + yield 1 + yield 2 + } + + CollectionAssert.AreEqual ([| 1; 2 |], SortedSet.toArray set, "sortedSet must add each distinct element once") + + [] + member _.``implicit yields add the elements in the order of Set`` () = + let set = sortedSet { + "b" + "B" + "a" + "A" + } + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray [| "b"; "B"; "a"; "A" |]), + SortedSet.toArray set, + "sortedSet must order the elements of implicit yields as Set does" + ) + + [] + member _.``yield! adds a sequence, an array, a sorted set and another expression`` () = + let inner = sortedSet { yield 7 } + + let set = sortedSet { + // A list is the sequence that the overload of sequences takes + yield! [ 2; 1 ] + yield! [| 4; 3 |] + yield! SortedSet.ofArray [| 6; 5; 1 |] + yield! inner + } + + CollectionAssert.AreEqual ([| 1..7 |], SortedSet.toArray set, "sortedSet 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 ($"sortedSet {{ %s{name} }}", paramName, actual) + + [] + member _.``yield! and for take a flat list of strings that is not default as a sequence of comparables`` () = + let widened = stringsAsComparables (FlatList.ofArray [| "b"; "a" |]) + + let set = sortedSet { + yield! widened + + for item in widened do + yield item + } + + CollectionAssert.AreEqual ( + [| ("a" :> IComparable); ("b" :> IComparable) |], + SortedSet.toArray set, + "sortedSet must take the elements of a flat list of strings as comparables in yield! and for" + ) + + [] + member _.``for adds the elements of the body for each item`` () = + let set = sortedSet { + for item in -2 .. 2 do + yield item * item + } + + CollectionAssert.AreEqual ([| 0; 1; 4 |], SortedSet.toArray set, "sortedSet must run the body of for for each item") + + [] + member _.``while adds the elements while the guard holds`` () = + let mutable count = 3 + + let set = sortedSet { + while count > 0 do + yield count + count <- count - 1 + } + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + SortedSet.toArray set, + "sortedSet must run the body of while while the guard holds" + ) + + [] + member _.``if without else and following elements combine`` () = + let set = sortedSet { + if false then + yield 0 + + yield 2 + + if true then + yield 1 + } + + CollectionAssert.AreEqual ([| 1; 2 |], SortedSet.toArray set, "sortedSet must combine conditional and plain elements") + + [] + member _.``try with adds the elements of the handler after an exception`` () = + let set = sortedSet { + try + yield 2 + failwith "fails" + with _ -> + yield 1 + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + SortedSet.toArray set, + "sortedSet must keep the elements before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let set = sortedSet { + try + yield 1 + finally + compensated <- true + } + + Assert.IsTrue (compensated, "sortedSet must run the compensation of try finally") + CollectionAssert.AreEqual ([| 1 |], SortedSet.toArray set, "sortedSet must keep the elements of the body of try finally") + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let set = sortedSet { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield 1 + } + + Assert.IsTrue (disposed, "sortedSet must dispose the resource of use") + CollectionAssert.AreEqual ([| 1 |], SortedSet.toArray set, "sortedSet must keep the elements of the body of use") + + [] + member _.``sortedSet compares with F# generic comparison`` () = + let set = sortedSet { + "apple" + "APPLE" + } + + Assert.AreSame ( + LanguagePrimitives.FastGenericComparer, + SortedSet.comparer set, + "sortedSet must compare with F# generic comparison" + ) + + CollectionAssert.AreEqual ([| "APPLE"; "apple" |], SortedSet.toArray set, "sortedSet must order ordinally") + + [] + member _.``a builder with a comparer builds sorted sets that order with it`` () = + let ignoreCase : IComparer = StringComparer.OrdinalIgnoreCase + + let set = SortedSetCE ignoreCase { + "cherry" + "apple" + "APPLE" + "Banana" + } + + Assert.AreSame (ignoreCase, SortedSet.comparer set, "SortedSetCE must build sorted sets with its comparer") + + CollectionAssert.AreEqual ( + [| "apple"; "Banana"; "cherry" |], + SortedSet.toArray set, + "SortedSetCE must order with its comparer and keep the first of the elements equal in it" + ) + + [] + member _.``a builder rejects a null comparer when it is created`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + SortedSetCE (Unchecked.defaultof>) + |> ignore + ), + "SortedSetCE must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "SortedSetCE must name the parameter comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetContractTests.fs new file mode 100644 index 00000000..7e72147b --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetContractTests.fs @@ -0,0 +1,26 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +// The ops records read every sorted set through SortedSetOps.toList, which checks that it enumerates in the order of Set. + +/// The reading contract for `SortedSet`. +[] +type SortedSetSetTests () = + inherit SetContract> (SortedSetOps.reading) + +/// The transformation contract for `SortedSet`. +[] +type SortedSetTransformTests () = + inherit SetTransformContract> (SortedSetOps.transform) + +/// The mutation contract for `SortedSet`. +[] +type SortedSetMutationTests () = + inherit SetMutationContract> (SortedSetOps.mutation) + +/// The creation contract for `SortedSet`. +[] +type SortedSetCreationTests () = + inherit SetCreationContract> (SortedSetOps.creation) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOps.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOps.fs new file mode 100644 index 00000000..bea85411 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOps.fs @@ -0,0 +1,144 @@ +/// The functions of the `SortedSet` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.SortedSetOps + +open System +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// Maps the name of a parameter of the FSharp.Core Set module to the name in the SortedSet module. +/// +/// +/// Set.minElement and Set.maxElement throw from the tree of the F# set, which names the set s. The +/// other names map as in SetParamNames.ofFSharpCore. +/// +let paramName (name : string) = + match name with + | "s" -> "set" + | name -> SetParamNames.ofFSharpCore name + +/// +/// The elements of a sorted set, after a check that the sorted set enumerates them in ascending order of its comparer. +/// +/// +/// The contracts compare the elements of a set as a sorted array, because a hash set enumerates them in an unspecified +/// order. A sorted set must enumerate them in ascending order of its comparer, so the contracts check it through this +/// function on every sorted set that they read. For the sorted sets that the contracts create without a comparer, that +/// is the order of Set; the struct of forComparer orders by remainders. +/// +let toArray (set : SortedSet<'T>) = + let elements = SortedSet.toArray set + let comparer = SortedSet.comparer set + + Assert.IsTrue ( + elements + |> Array.pairwise + |> Array.forall (fun (previous, next) -> comparer.Compare (previous, next) < 0), + "A sorted set must enumerate its distinct elements in ascending order of its comparer" + ) + + elements + +let private toArrayPair (struct (first : SortedSet<'T1>, second : SortedSet<'T2>)) = struct (toArray first, toArray second) + +/// Creates sorted sets that order by remainders: the struct of `forComparer` with `SetInputs.ByRemainder`. +let private ofSeqByRemainder (elements : seq) = (SortedSet.forComparer SetInputs.ByRemainder).ofSeq elements + +/// The functions of the reading facet. +let reading : SetOps> = { + Name = nameof SortedSet + ParamName = paramName + OfSeq = SortedSet.ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = toArray + + Count = SortedSet.count + IsEmpty = SortedSet.isEmpty + + TryGetValue = SortedSet.tryGetValue + + Contains = SortedSet.contains + Exists = SortedSet.exists + Forall = SortedSet.forall + IsProperSubset = SortedSet.isProperSubset + IsProperSuperset = SortedSet.isProperSuperset + IsSubset = SortedSet.isSubset + IsSuperset = SortedSet.isSuperset + Overlaps = SortedSet.overlaps + SetEquals = SortedSet.setEquals + + Fold = SortedSet.fold + FoldBack = SortedSet.foldBack + Iter = SortedSet.iter + + ToList = SortedSet.toList + ToSeq = SortedSet.toSeq +} + +/// The functions of the transformation facet. +let transform : SetTransformOps> = { + Name = nameof SortedSet + ParamName = paramName + OfSeq = SortedSet.ofSeq + ToArray = toArray + + Choose = fun chooser set -> SortedSet.choose chooser set |> toArray + Difference = SortedSet.difference + Filter = SortedSet.filter + Intersect = SortedSet.intersect + IntersectMany = SortedSet.intersectMany + Map = fun mapping set -> SortedSet.map mapping set |> toArray + SymmetricDifference = SortedSet.symmetricDifference + Union = SortedSet.union + UnionMany = SortedSet.unionMany + + Partition = SortedSet.partition + PartitionWith = fun partitioner set -> SortedSet.partitionWith partitioner set |> toArrayPair +} + +/// The functions of the mutation facet. +let mutation : SetMutationOps> = { + Name = nameof SortedSet + ParamName = paramName + OfSeq = SortedSet.ofSeq + OfSeqByRemainder = ofSeqByRemainder + ToArray = toArray + + Add = SortedSet.add + AddRange = SortedSet.addRange + Clear = SortedSet.clear + Remove = SortedSet.remove + RemoveRange = SortedSet.removeRange +} + +/// The functions of the creation facet; the struct of `forComparer` orders with `SetInputs.ByRemainder`. +let creation : SetCreationOps> = + let custom = SortedSet.forComparer SetInputs.ByRemainder + + { + Name = nameof SortedSet + ParamName = paramName + ToArray = toArray + Comparer = fun set -> SortedSet.comparer set :> obj + + Empty = fun () -> SortedSet.empty + OfArray = SortedSet.ofArray + OfList = SortedSet.ofList + OfReadOnlySpan = fun array -> SortedSet.ofReadOnlySpan (ReadOnlySpan array) + OfSeq = SortedSet.ofSeq + OfSpan = fun array -> SortedSet.ofSpan (Span array) + Singleton = SortedSet.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/SortedSet/SortedSetOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOptionTests.fs new file mode 100644 index 00000000..217f7475 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetOptionTests.fs @@ -0,0 +1,73 @@ +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 `SortedSet` module of the `Option` namespace. +[] +type SortedSetOptionTests () = + + static let numbers = SortedSet.ofArray Inputs.Several + + static let negatedIfNegative (item : int) = if item < 0 then Some (-item) else None + + /// The indices of tryItem, as rows: before the first element, the first one, one inside and the number of elements. + static member Indices = seq { + -1 + 0 + 3 + (Set.ofArray Inputs.Several).Count + } + + [] + member _.``tryGetValue returns an option`` (value : int) = + let found : int option = SortedSet.tryGetValue value numbers + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.SortedSet.tryGetValue value numbers, + found, + "Option.SortedSet.tryGetValue must match the default" + ) + + [] + member _.``tryItem returns an option`` (index : int) = + let found : int option = SortedSet.tryItem index numbers + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.SortedSet.tryItem index numbers, + found, + "Option.SortedSet.tryItem must match the default" + ) + + [] + member _.``choose takes a chooser of options`` () = + let chosen : SortedSet = SortedSet.choose negatedIfNegative numbers + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray (Array.choose negatedIfNegative Inputs.Several)), + SortedSet.toArray chosen, + "Option.SortedSet.choose must keep the distinct values that Array.choose chooses, in the order of Set" + ) + + [] + member _.``the functions without an option keep resolving to the default module`` () = + let mapped : SortedSet = SortedSet.map (fun item -> item * 2) numbers + let (accepted : SortedSet), (rejected : SortedSet) = + SortedSet.partition (fun item -> item > 0) numbers + + CollectionAssert.AreEqual ( + Set.toArray (Set.map (fun item -> item * 2) (Set.ofArray Inputs.Several)), + SortedSet.toArray mapped, + "SortedSet.map must resolve to the default module" + ) + + Assert.AreEqual( + SortedSet.count numbers, + SortedSet.count accepted + SortedSet.count rejected, + "SortedSet.partition must resolve to the Tuple module" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs new file mode 100644 index 00000000..f7752e2c --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTests.fs @@ -0,0 +1,1153 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Globalization +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// A creation function of SortedSet without a comparer, as a row of the tests that run on each one. +/// +/// +/// The rows are union cases, because MSTest names a row after its values and a function prints no readable value. A case +/// creates sorted sets of any element type with F# comparison, which a table of functions of one type cannot do. +/// +type SortedSetCreation = + /// SortedSet.empty, which ignores the elements. + | Empty + /// SortedSet.ofArray. + | OfArray + /// SortedSet.ofList, with the elements in a list. + | OfList + /// SortedSet.ofReadOnlySpan, with a read-only span over the elements. + | OfReadOnlySpan + /// SortedSet.ofSeq, with the elements in a lazy sequence. + | OfSeq + /// SortedSet.ofSpan, with a span over the elements. + | OfSpan + /// SortedSet.add of every element to SortedSet.empty. + | Add + /// SortedSet.singleton of every element, united by SortedSet.unionMany. + | Singleton + /// SortedSet.build with an action that adds the elements to the builder. + | Build + /// A builder of SortedSet.builder filled with the elements, through SortedSet.ofBuilder. + | Builder + + /// Creates a sorted set of the elements with the function of the case. + member this.Create<'T when 'T : comparison> (elements : 'T array) : SortedSet<'T> = + match this with + | Empty -> SortedSet.empty + | OfArray -> SortedSet.ofArray elements + // The function under test takes a list + | OfList -> SortedSet.ofList (List.ofArray elements) + | OfReadOnlySpan -> SortedSet.ofReadOnlySpan (ReadOnlySpan<'T> elements) + | OfSeq -> SortedSet.ofSeq (Seq.map id elements) + | OfSpan -> SortedSet.ofSpan (Span<'T> elements) + | Add -> Array.fold (fun set item -> SortedSet.add item set) SortedSet.empty elements + | Singleton -> + elements + |> Array.map SortedSet.singleton + |> SortedSet.unionMany + | Build -> SortedSet.build (fun builder -> builder.UnionWith elements) + | Builder -> + let builder = SortedSet.builder () + builder.UnionWith elements + SortedSet.ofBuilder builder + +/// +/// The behaviour of SortedSet that the shared contracts do not cover: the order of F# generic comparison, the +/// comparers, the struct of forComparer, the sorted-only functions, shared storage 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 neither a function nor a sorted set prints a readable value. +/// +[] +type SortedSetTests () = + + static let ignoreCase : IComparer = StringComparer.OrdinalIgnoreCase + + /// Creates sorted sets that order strings without case. + static let caseless = SortedSet.forComparer ignoreCase + + /// Creates sorted sets of pairs that order the pairs by their numbers alone, so that pairs with the same number + /// compare equal. + static let byKey = + SortedSet.forComparer ( + ComparisonIdentity.FromFunction (fun ((left : int), (_ : string)) (right, _) -> compare left right) + ) + + /// The comparer of the sorted sets of strings created without one. FSharp.Core keeps one comparer per primitive type + /// and per string, so the comparer resolved here is the one of the module. + static let defaultComparer : IComparer = LanguagePrimitives.FastGenericComparer + + /// Orders the numbers from the largest one to the smallest one. + static let descending : IComparer = ComparisonIdentity.FromFunction (fun left right -> compare right left) + + /// Strings in an order that differs between ordinal comparison, which F# generic comparison uses, and the culture. + static let words = [| "b"; "B"; "a"; "A"; "é"; "e"; "Z"; "z" |] + + /// Numbers with duplicates, negative numbers and zero. + static let numbers = [| 5; -3; 8; 5; 0; -3; 12; 7; -10 |] + + /// Pairs whose strings decide the order: F# generic comparison compares them ordinally, while the default comparer of + /// a tuple compares them in the culture. + static let pairs = [| (1, "b"); (0, "z"); (1, "B"); (1, "a"); (-1, "a"); (0, "z") |] + + /// Words that compare without case, so that a value can equal an element that differs from it. + static let fruits () = caseless.ofArray [| "cherry"; "apple"; "Banana" |] + + 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> + + /// Adds the elements one by one to a sorted set. + static let addAll (set : SortedSet) (elements : string array) = + Array.fold (fun set item -> SortedSet.add item set) set elements + + /// The sorted set of a builder of the struct that added the elements. + static let ofCaselessBuilder (elements : string array) = + let builder = caseless.builder () + builder.UnionWith elements + SortedSet.ofBuilder builder + + /// + /// The members of the struct, by name, each with the sorted set that it creates from "cherry", "apple", "APPLE" and + /// "Banana". + /// + /// + /// empty and singleton get the other elements through SortedSet.add. + /// + static let structCreations : Map SortedSet> = + Map.ofArray [| + nameof caseless.empty, (fun () -> addAll caseless.empty [| "cherry"; "apple"; "APPLE"; "Banana" |]) + nameof caseless.ofArray, (fun () -> caseless.ofArray [| "cherry"; "apple"; "APPLE"; "Banana" |]) + // The member under test takes a list + nameof caseless.ofList, (fun () -> caseless.ofList [ "cherry"; "apple"; "APPLE"; "Banana" ]) + nameof caseless.ofReadOnlySpan, + (fun () -> caseless.ofReadOnlySpan (ReadOnlySpan [| "cherry"; "apple"; "APPLE"; "Banana" |])) + nameof caseless.ofSeq, (fun () -> caseless.ofSeq (Seq.ofArray [| "cherry"; "apple"; "APPLE"; "Banana" |])) + nameof caseless.ofSpan, (fun () -> caseless.ofSpan (Span [| "cherry"; "apple"; "APPLE"; "Banana" |])) + nameof caseless.singleton, (fun () -> addAll (caseless.singleton "cherry") [| "apple"; "APPLE"; "Banana" |]) + nameof caseless.build, + (fun () -> caseless.build (fun builder -> builder.UnionWith [| "cherry"; "apple"; "APPLE"; "Banana" |])) + nameof caseless.builder, (fun () -> ofCaselessBuilder [| "cherry"; "apple"; "APPLE"; "Banana" |]) + |] + + /// The members of the struct that take elements, by name, with the struct that orders pairs by their numbers. + static let byKeyCreations : Map SortedSet> = + Map.ofArray [| + nameof byKey.ofArray, (fun input -> byKey.ofArray input) + // The member under test takes a list + nameof byKey.ofList, (fun input -> byKey.ofList (List.ofArray input)) + nameof byKey.ofReadOnlySpan, (fun input -> byKey.ofReadOnlySpan (ReadOnlySpan input)) + nameof byKey.ofSeq, (fun input -> byKey.ofSeq (Seq.ofArray input)) + nameof byKey.ofSpan, (fun input -> byKey.ofSpan (Span input)) + |] + + /// The incremental changes, by name, applied to the fruits. + static let changes : Map -> SortedSet> = + Map.ofArray [| + nameof SortedSet.add, SortedSet.add "date" + nameof SortedSet.addRange, SortedSet.addRange [| "APPLE"; "date" |] + nameof SortedSet.clear, SortedSet.clear + nameof SortedSet.remove, SortedSet.remove "APPLE" + nameof SortedSet.removeRange, SortedSet.removeRange [| "APPLE"; "date" |] + nameof SortedSet.update, SortedSet.update (fun builder -> builder.Add "date" |> ignore) + |] + + /// The elements of the fruits that are long, through partition. + static let longFruits (set : SortedSet) = + let struct (accepted, _) = SortedSet.partition isLong set + accepted + + /// The elements of the fruits that are short, through partition. + static let shortFruits (set : SortedSet) = + let struct (_, rejected) = SortedSet.partition isLong set + rejected + + /// The functions that select elements, by name, with the elements that they return for the fruits. + static let selections : Map -> SortedSet) * string array)> = + Map.ofArray [| + nameof SortedSet.filter, struct (SortedSet.filter isLong, [| "Banana"; "cherry" |]) + $"%s{nameof SortedSet.partition} (accepted)", struct (longFruits, [| "Banana"; "cherry" |]) + $"%s{nameof SortedSet.partition} (rejected)", struct (shortFruits, [| "apple" |]) + |] + + /// The set algebra, by name, with the elements that it returns for the fruits and the sorted set of "APPLE" and "date". + static let algebra : Map -> SortedSet -> SortedSet) * string array)> = + Map.ofArray [| + nameof SortedSet.difference, struct (SortedSet.difference, [| "Banana"; "cherry" |]) + // Intersect keeps the elements of the second set + nameof SortedSet.intersect, struct (SortedSet.intersect, [| "APPLE" |]) + nameof SortedSet.symmetricDifference, struct (SortedSet.symmetricDifference, [| "Banana"; "cherry"; "date" |]) + nameof SortedSet.union, struct (SortedSet.union, [| "apple"; "Banana"; "cherry"; "date" |]) + |] + + /// The first choices of partitionWith for the fruits. + static let firstChoices (set : SortedSet) = + let struct (first, _) = SortedSet.partitionWith byLength set + first + + /// The second choices of partitionWith for the fruits. + static let secondChoices (set : SortedSet) = + let struct (_, second) = SortedSet.partitionWith byLength set + second + + /// The transformations to another element type, by name, with the elements that they return for the fruits. + static let typeChanges : Map -> SortedSet) * string array)> = + Map.ofArray [| + nameof SortedSet.map, struct (SortedSet.map id, [| "Banana"; "apple"; "cherry" |]) + nameof SortedSet.choose, struct (SortedSet.choose ValueSome, [| "Banana"; "apple"; "cherry" |]) + $"%s{nameof SortedSet.partitionWith} (first)", struct (firstChoices, [| "Banana"; "cherry" |]) + $"%s{nameof SortedSet.partitionWith} (second)", struct (secondChoices, [| "apple" |]) + |] + + /// The functions that take a sorted set, by name and parameter, with the name of the parameter and a call with null. + static let nullRejections : Map unit))> = + let set = SortedSet.ofArray Inputs.Several + + Map.ofArray [| + nameof SortedSet.add, struct ("set", (fun () -> SortedSet.add 1 nullSet |> ignore)) + nameof SortedSet.addRange, struct ("set", (fun () -> SortedSet.addRange [| 1 |] nullSet |> ignore)) + nameof SortedSet.choose, struct ("set", (fun () -> SortedSet.choose ValueSome nullSet |> ignore)) + nameof SortedSet.clear, struct ("set", (fun () -> SortedSet.clear nullSet |> ignore)) + nameof SortedSet.comparer, struct ("set", (fun () -> SortedSet.comparer nullSet |> ignore)) + nameof SortedSet.contains, struct ("set", (fun () -> SortedSet.contains 1 nullSet |> ignore)) + nameof SortedSet.count, struct ("set", (fun () -> SortedSet.count nullSet |> ignore)) + $"%s{nameof SortedSet.difference} (set1)", struct ("set1", (fun () -> SortedSet.difference nullSet set |> ignore)) + $"%s{nameof SortedSet.difference} (set2)", struct ("set2", (fun () -> SortedSet.difference set nullSet |> ignore)) + nameof SortedSet.exists, struct ("set", (fun () -> SortedSet.exists ((=) 1) nullSet |> ignore)) + nameof SortedSet.filter, struct ("set", (fun () -> SortedSet.filter ((=) 1) nullSet |> ignore)) + nameof SortedSet.fold, struct ("set", (fun () -> SortedSet.fold (+) 0 nullSet |> ignore)) + nameof SortedSet.foldBack, struct ("set", (fun () -> SortedSet.foldBack (+) nullSet 0 |> ignore)) + nameof SortedSet.forall, struct ("set", (fun () -> SortedSet.forall ((=) 1) nullSet |> ignore)) + nameof SortedSet.indexOf, struct ("set", (fun () -> SortedSet.indexOf 1 nullSet |> ignore)) + $"%s{nameof SortedSet.intersect} (set1)", struct ("set1", (fun () -> SortedSet.intersect nullSet set |> ignore)) + $"%s{nameof SortedSet.intersect} (set2)", struct ("set2", (fun () -> SortedSet.intersect set nullSet |> ignore)) + nameof SortedSet.intersectMany, struct ("sets", (fun () -> SortedSet.intersectMany [| set; nullSet |] |> ignore)) + nameof SortedSet.isEmpty, struct ("set", (fun () -> SortedSet.isEmpty nullSet |> ignore)) + $"%s{nameof SortedSet.isProperSubset} (set1)", + struct ("set1", (fun () -> SortedSet.isProperSubset nullSet set |> ignore)) + $"%s{nameof SortedSet.isProperSubset} (set2)", + struct ("set2", (fun () -> SortedSet.isProperSubset set nullSet |> ignore)) + $"%s{nameof SortedSet.isProperSuperset} (set1)", + struct ("set1", (fun () -> SortedSet.isProperSuperset nullSet set |> ignore)) + $"%s{nameof SortedSet.isProperSuperset} (set2)", + struct ("set2", (fun () -> SortedSet.isProperSuperset set nullSet |> ignore)) + $"%s{nameof SortedSet.isSubset} (set1)", struct ("set1", (fun () -> SortedSet.isSubset nullSet set |> ignore)) + $"%s{nameof SortedSet.isSubset} (set2)", struct ("set2", (fun () -> SortedSet.isSubset set nullSet |> ignore)) + $"%s{nameof SortedSet.isSuperset} (set1)", struct ("set1", (fun () -> SortedSet.isSuperset nullSet set |> ignore)) + $"%s{nameof SortedSet.isSuperset} (set2)", struct ("set2", (fun () -> SortedSet.isSuperset set nullSet |> ignore)) + nameof SortedSet.item, struct ("set", (fun () -> SortedSet.item 0 nullSet |> ignore)) + nameof SortedSet.iter, struct ("set", (fun () -> SortedSet.iter ignore nullSet)) + nameof SortedSet.map, struct ("set", (fun () -> SortedSet.map id nullSet |> ignore)) + nameof SortedSet.maxElement, struct ("set", (fun () -> SortedSet.maxElement nullSet |> ignore)) + nameof SortedSet.minElement, struct ("set", (fun () -> SortedSet.minElement nullSet |> ignore)) + $"%s{nameof SortedSet.overlaps} (set1)", struct ("set1", (fun () -> SortedSet.overlaps nullSet set |> ignore)) + $"%s{nameof SortedSet.overlaps} (set2)", struct ("set2", (fun () -> SortedSet.overlaps set nullSet |> ignore)) + nameof SortedSet.partition, struct ("set", (fun () -> SortedSet.partition ((=) 1) nullSet |> ignore)) + nameof SortedSet.partitionWith, + struct ("set", + (fun () -> + SortedSet.partitionWith (fun (item : int) -> Choice.Choice1Of2 item) nullSet + |> ignore + )) + nameof SortedSet.remove, struct ("set", (fun () -> SortedSet.remove 1 nullSet |> ignore)) + nameof SortedSet.removeRange, struct ("set", (fun () -> SortedSet.removeRange [| 1 |] nullSet |> ignore)) + nameof SortedSet.rev, struct ("set", (fun () -> SortedSet.rev nullSet |> ignore)) + $"%s{nameof SortedSet.setEquals} (set1)", struct ("set1", (fun () -> SortedSet.setEquals nullSet set |> ignore)) + $"%s{nameof SortedSet.setEquals} (set2)", struct ("set2", (fun () -> SortedSet.setEquals set nullSet |> ignore)) + $"%s{nameof SortedSet.symmetricDifference} (set1)", + struct ("set1", (fun () -> SortedSet.symmetricDifference nullSet set |> ignore)) + $"%s{nameof SortedSet.symmetricDifference} (set2)", + struct ("set2", (fun () -> SortedSet.symmetricDifference set nullSet |> ignore)) + nameof SortedSet.toArray, struct ("set", (fun () -> SortedSet.toArray nullSet |> ignore)) + nameof SortedSet.toBuilder, struct ("set", (fun () -> SortedSet.toBuilder nullSet |> ignore)) + nameof SortedSet.toList, struct ("set", (fun () -> SortedSet.toList nullSet |> ignore)) + nameof SortedSet.toSeq, struct ("set", (fun () -> SortedSet.toSeq nullSet |> ignore)) + nameof SortedSet.tryGetValue, struct ("set", (fun () -> SortedSet.tryGetValue 1 nullSet |> ignore)) + nameof SortedSet.tryItem, struct ("set", (fun () -> SortedSet.tryItem 0 nullSet |> ignore)) + $"%s{nameof SortedSet.union} (set1)", struct ("set1", (fun () -> SortedSet.union nullSet set |> ignore)) + $"%s{nameof SortedSet.union} (set2)", struct ("set2", (fun () -> SortedSet.union set nullSet |> ignore)) + nameof SortedSet.unionMany, struct ("sets", (fun () -> SortedSet.unionMany [| nullSet; set |] |> ignore)) + nameof SortedSet.update, struct ("set", (fun () -> SortedSet.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 = SortedSet.ofArray Inputs.Several + let ints = SortedSet.forComparer (Comparer.Default :> IComparer) + let objectComparer = SortedSet.forComparer (Comparer.Default :> IComparer) + 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. obj + // has no F# comparison, so the sorted sets of objects and the creation from objects come from the struct. + let widenedElements = FlatListOracles.defaultStringsAsObjects + let objects = objectComparer.ofArray [| ("a" :> obj) |] + + Map.ofArray [| + $"%s{nameof SortedSet.addRange} (null elements)", + struct ("elements", (fun () -> SortedSet.addRange nullElements set |> ignore)) + $"%s{nameof SortedSet.addRange} (default flat list)", + struct ("elements", (fun () -> SortedSet.addRange defaultElements set |> ignore)) + $"%s{nameof SortedSet.addRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> SortedSet.addRange widenedElements objects |> ignore)) + $"%s{nameof SortedSet.intersectMany} (null sets)", + struct ("sets", (fun () -> SortedSet.intersectMany nullSets |> ignore)) + $"%s{nameof SortedSet.intersectMany} (default flat list)", + struct ("sets", (fun () -> SortedSet.intersectMany defaultSets |> ignore)) + $"%s{nameof SortedSet.ofSeq} (null elements)", struct ("elements", (fun () -> SortedSet.ofSeq nullElements |> ignore)) + $"%s{nameof SortedSet.ofSeq} (default flat list)", + struct ("elements", (fun () -> SortedSet.ofSeq defaultElements |> ignore)) + $"%s{nameof SortedSet.removeRange} (null elements)", + struct ("elements", (fun () -> SortedSet.removeRange nullElements set |> ignore)) + $"%s{nameof SortedSet.removeRange} (default flat list)", + struct ("elements", (fun () -> SortedSet.removeRange defaultElements set |> ignore)) + $"%s{nameof SortedSet.removeRange} (default flat list of strings as objects)", + struct ("elements", (fun () -> SortedSet.removeRange widenedElements objects |> ignore)) + $"%s{nameof SortedSet.unionMany} (null sets)", struct ("sets", (fun () -> SortedSet.unionMany nullSets |> ignore)) + $"%s{nameof SortedSet.unionMany} (default flat list)", + struct ("sets", (fun () -> SortedSet.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)) + $"forComparer %s{nameof ints.ofSeq} (default flat list of strings as objects)", + struct ("elements", (fun () -> objectComparer.ofSeq widenedElements |> ignore)) + |] + + /// The creation functions that create a sorted set of the elements, as rows. + static member Creations = seq { + OfArray + OfList + OfReadOnlySpan + OfSeq + OfSpan + Add + Singleton + Build + Builder + } + + /// Every creation function without a comparer, the empty sorted set included, as rows. + static member DefaultCreations = seq { + Empty + yield! SortedSetTests.Creations + } + + /// The creation functions that take the elements as they are, as rows. + static member ElementCreations = seq { + OfArray + OfList + OfReadOnlySpan + OfSeq + OfSpan + } + + /// The names of the members of the struct that create sorted sets, as rows. + static member StructCreations = structCreations.Keys + + /// The names of the members of the struct that take elements, as rows. + static member ByKeyCreations = byKeyCreations.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 sorted 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 + + /// The indices outside the numbers: before the first one, the number of distinct numbers and the largest index. + static member OutOfRangeIndices = seq { + -1 + (Set.ofArray numbers).Count + Int32.MaxValue + } + + /// The indices of tryItem with the element at each one or nothing: inside and on both sides of the numbers. + static member TryItemCases = seq { + let last = (Set.ofArray numbers).Count - 1 + struct (-1, ValueNone) + struct (0, ValueSome -10) + struct (3, ValueSome 5) + struct (last, ValueSome 12) + struct (last + 1, ValueNone) + } + + // ---- Order ---- + + [] + member _.``strings compare ordinally as in Set`` (creation : SortedSetCreation) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray words), + SortedSet.toArray (creation.Create words), + "The creation function must order strings as Set does" + ) + + [] + member _.``strings sort ordinally, not in the order of the culture`` () = + CollectionAssert.AreEqual ( + [| "A"; "B"; "Z"; "a"; "b"; "e"; "z"; "é" |], + SortedSet.toArray (SortedSet.ofArray words), + "SortedSet.ofArray must order strings ordinally, not in the order of the culture" + ) + + [] + member _.``negative numbers come first as in Set`` (creation : SortedSetCreation) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray numbers), + SortedSet.toArray (creation.Create numbers), + "The creation function must order numbers as Set does" + ) + + [] + member _.``numbers sort in ascending order`` () = + CollectionAssert.AreEqual ( + [| -10; -3; 0; 5; 7; 8; 12 |], + SortedSet.toArray (SortedSet.ofArray numbers), + "SortedSet.ofArray must order the numbers in ascending order" + ) + + [] + member _.``tuples compare their strings ordinally as in Set`` (creation : SortedSetCreation) = + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray pairs), + SortedSet.toArray (creation.Create pairs), + "The creation function must order tuples as Set does" + ) + + [] + member _.``tuples sort their strings ordinally`` () = + CollectionAssert.AreEqual ( + [| (-1, "a"); (0, "z"); (1, "B"); (1, "a"); (1, "b") |], + SortedSet.toArray (SortedSet.ofArray pairs), + "SortedSet.ofArray must order tuples in F# generic comparison" + ) + + [] + member _.``ofArray, ofList, ofReadOnlySpan, ofSeq and ofSpan keep the first of the elements that compare equal, as Set does`` + (creation : SortedSetCreation) + = + // F# generic comparison considers 0.0 and -0.0 equal, while the sign tells them apart + let input = [| + for round in 0..1 do + for index in 0..99 do + let key = index * 37 % 100 + let negative = (key + round) % 2 = 0 + (key, (if negative then -0.0 else 0.0)) + |] + + let signs (elements : (int * float) array) = + elements + |> Array.map (fun (key, value) -> key, Double.IsNegative value) + + CollectionAssert.AreEqual ( + signs (Set.toArray (Set.ofArray input)), + signs (SortedSet.toArray (creation.Create input)), + "The creation function must keep the first of the elements that compare equal, as Set does" + ) + + [] + member _.``foldBack visits the elements in descending order as Set.foldBack does`` () = + let visitedBy (foldBack : (string -> unit -> unit) -> unit -> unit) = + let visited = ResizeArray() + foldBack (fun item () -> visited.Add item) () + visited.ToArray () + + let expected = visitedBy (fun folder state -> Set.foldBack folder (Set.ofArray words) state) + let actual = visitedBy (fun folder state -> SortedSet.foldBack folder (SortedSet.ofArray words) state) + + CollectionAssert.AreEqual (expected, actual, "SortedSet.foldBack must visit the elements as Set.foldBack does") + + CollectionAssert.AreEqual ( + [| "é"; "z"; "e"; "b"; "a"; "Z"; "B"; "A" |], + actual, + "SortedSet.foldBack must visit the elements in descending order" + ) + + [] + member _.``partitionWith keeps the same one of two equal values as Set.partitionWith`` () = + // F# generic comparison considers 0.0 and -0.0 equal. The smaller element of each pair of choices gives -0.0, and + // Set.partitionWith keeps the value of the larger one, because it visits the elements from the largest one + let partitioner (item : int) = + let value = if item < 3 then -0.0 else 0.0 + if item % 2 = 0 then Choice1Of2 value else Choice2Of2 value + + let input = [| 1; 2; 3; 4 |] + let expectedFirst, expectedSecond = Set.partitionWith partitioner (Set.ofArray input) + let struct (first, second) = SortedSet.partitionWith partitioner (SortedSet.ofArray input) + + Assert.AreEqual( + Double.IsNegative (Set.minElement expectedFirst), + Double.IsNegative (SortedSet.minElement first), + "SortedSet.partitionWith must keep the first choice that Set.partitionWith keeps" + ) + + Assert.AreEqual( + Double.IsNegative (Set.minElement expectedSecond), + Double.IsNegative (SortedSet.minElement second), + "SortedSet.partitionWith must keep the second choice that Set.partitionWith keeps" + ) + + [] + member _.``rev returns the elements in descending order`` () = + CollectionAssert.AreEqual ( + Array.rev (Set.toArray (Set.ofArray numbers)), + Seq.toArray (SortedSet.rev (SortedSet.ofArray numbers)), + "SortedSet.rev must return the elements from the largest one" + ) + + CollectionAssert.AreEqual ( + [| "cherry"; "Banana"; "apple" |], + Seq.toArray (SortedSet.rev (fruits ())), + "SortedSet.rev must follow the comparer of the sorted set" + ) + + Assert.IsEmpty (SortedSet.rev SortedSet.empty, "SortedSet.rev of an empty sorted set must be empty") + + // ---- Comparers ---- + + [] + member _.``creation without a comparer compares with F# generic comparison`` (creation : SortedSetCreation) = + Assert.AreSame ( + defaultComparer, + SortedSet.comparer (creation.Create [| "a" |]), + "The creation function must compare with F# generic comparison" + ) + + [] + member _.``the sorted sets of one element type share one comparer`` (creation : SortedSetCreation) = + // FastGenericComparer creates a new comparer for each call at a concrete type that is not primitive, which an + // optimized build makes of an inlined function; the module resolves the comparer once per type instead + Assert.AreSame ( + SortedSet.comparer SortedSet.empty, + SortedSet.comparer (creation.Create pairs), + "The creation function must compare with the comparer of the type" + ) + + [] + member _.``forComparer creates the struct with the comparer`` () = + Assert.AreSame (ignoreCase, (SortedSet.forComparer ignoreCase).Comparer, "SortedSet.forComparer must keep the comparer") + + Assert.AreSame ( + ignoreCase, + (CustomComparerSortedSet ignoreCase).Comparer, + "The constructor of CustomComparerSortedSet must keep the comparer" + ) + + [] + member _.``forComparer orders an element type without F# comparison`` () = + // Functions do not support F# comparison, so only a comparer can order them: here by their results for 0 + let byResult = + ComparisonIdentity.FromFunction (fun (left : int -> int) right -> compare (left 0) (right 0)) + + let set = + (SortedSet.forComparer byResult).ofArray [| (fun x -> x + 2); (fun x -> x + 1); (fun x -> x + 3) |] + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + SortedSet.toArray set |> Array.map (fun item -> item 0), + "CustomComparerSortedSet.ofArray must order the elements with the comparer" + ) + + [] + member _.``the members of the struct create sorted sets that order with its comparer`` (name : string) = + let set = structCreations[name]() + + Assert.AreSame (ignoreCase, SortedSet.comparer set, $"CustomComparerSortedSet.%s{name} must keep the comparer") + + CollectionAssert.AreEqual ( + [| "apple"; "Banana"; "cherry" |], + SortedSet.toArray set, + $"CustomComparerSortedSet.%s{name} must order with the comparer and keep the first of the elements equal in it" + ) + + [] + member _.``the members of the struct keep the first of many elements that compare equal`` (name : string) = + // An unstable sort, as in ImmutableSortedSet.CreateRange, would keep a later element for some of the keys + let input = [| + for round in [| "first"; "second"; "third" |] do + for index in 0..99 do + (index * 37 % 100, round) + |] + + CollectionAssert.AreEqual ( + [| for key in 0..99 -> (key, "first") |], + SortedSet.toArray (byKeyCreations[name] input), + $"CustomComparerSortedSet.%s{name} must keep the first of the elements that compare equal" + ) + + [] + member _.``the struct gives an existing sorted set its comparer`` () = + let set = SortedSet.ofArray [| "apple"; "APPLE"; "Banana" |] + let ignoringCase = caseless.ofSeq set + + Assert.AreSame ( + ignoreCase, + SortedSet.comparer ignoringCase, + "CustomComparerSortedSet.ofSeq must give the sorted set the comparer" + ) + + // The sorted set enumerates "APPLE" before "apple", so the new one keeps "APPLE" + CollectionAssert.AreEqual ( + [| "APPLE"; "Banana" |], + SortedSet.toArray ignoringCase, + "CustomComparerSortedSet.ofSeq must keep the first of the elements that the comparer considers equal" + ) + + Assert.HasCount (3, set, "CustomComparerSortedSet.ofSeq must not change the sorted set") + + // The BCL tests the instance of the comparer, not its behaviour, so two equal comparers are two comparers (D14) + [] + member _.``the struct returns a sorted set only for the same comparer instance`` () = + let first : IComparer = StringComparer.Create (CultureInfo.InvariantCulture, true) + let second : IComparer = 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 = (SortedSet.forComparer first).ofArray [| "apple"; "Banana" |] + + Assert.AreSame ( + set, + (SortedSet.forComparer first).ofSeq set, + "CustomComparerSortedSet.ofSeq must return a sorted set that has the same comparer instance" + ) + + let rebuilt = (SortedSet.forComparer second).ofSeq set + + Assert.AreNotSame ( + set, + rebuilt, + "CustomComparerSortedSet.ofSeq must rebuild a sorted set whose comparer is another instance" + ) + + Assert.AreSame ( + second, + SortedSet.comparer rebuilt, + "CustomComparerSortedSet.ofSeq must give the rebuilt sorted set its comparer" + ) + + CollectionAssert.AreEqual ( + [| "apple"; "Banana" |], + SortedSet.toArray rebuilt, + "CustomComparerSortedSet.ofSeq must keep the elements" + ) + + [] + member _.``ofSeq gives a sorted set with another comparer F# generic comparison`` () = + let copy = SortedSet.ofSeq (fruits ()) + + Assert.AreSame (defaultComparer, SortedSet.comparer copy, "SortedSet.ofSeq must compare with F# generic comparison") + + CollectionAssert.AreEqual ( + [| "Banana"; "apple"; "cherry" |], + SortedSet.toArray copy, + "SortedSet.ofSeq must order the elements of a sorted set with another comparer ordinally" + ) + + [] + member _.``the default value of the struct orders with the default comparer of the BCL`` () = + // The struct cannot require F# comparison, so its default value falls back as the BCL does for a null comparer + let defaultValue = Unchecked.defaultof> + let bclDefault = Comparer.Default :> IComparer + let set = defaultValue.ofArray [| 3; 1; 2; 1 |] + + Assert.AreSame (bclDefault, defaultValue.Comparer, "The default CustomComparerSortedSet must return Comparer.Default") + Assert.AreSame (bclDefault, SortedSet.comparer set, "The default CustomComparerSortedSet must create sorted sets with it") + + Assert.AreSame ( + bclDefault, + SortedSet.comparer defaultValue.empty, + "The default CustomComparerSortedSet must create empty sorted sets with it" + ) + + CollectionAssert.AreEqual ( + [| 1; 2; 3 |], + SortedSet.toArray set, + "The default CustomComparerSortedSet must order with Comparer.Default" + ) + + [] + member _.``add, addRange, clear, remove, removeRange and update keep the comparer`` (name : string) = + Assert.AreSame (ignoreCase, SortedSet.comparer (changes[name](fruits ())), $"SortedSet.%s{name} must keep the comparer") + + [] + member _.``addRange adds the elements that are not equal in the comparer to an element of the sorted set`` () = + // "APPLE" equals "apple" without case, so the sorted set keeps "apple" (ADR 0001, D14) + CollectionAssert.AreEqual ( + [| "apple"; "Banana"; "cherry"; "date" |], + SortedSet.toArray (SortedSet.addRange [| "APPLE"; "date" |] (fruits ())), + "SortedSet.addRange must compare with the comparer of the sorted set" + ) + + // ImmutableSortedSet.Union refills the tree from an unstable sort when the sequence is large against the set, and then + // keeps an arbitrary one of the elements that compare equal (measured: 4 of 25 elements of the set kept for 250 added + // elements). The builder adds the elements one by one, so the element of the set stays whatever the size of the + // sequence. + [] + member _.``addRange keeps the elements of the sorted set whatever the size of the sequence`` (spellings : int) = + let expected = [| for key in 0..24 -> (key, "set") |] + let set = byKey.ofArray expected + + let added = [| + for round in 0 .. spellings - 1 do + for key in 0..24 do + (key, $"added%d{round}") + |] + + CollectionAssert.AreEqual ( + expected, + SortedSet.toArray (SortedSet.addRange added set), + "SortedSet.addRange must keep the element of the sorted set for every key" + ) + + // Union adds the smaller sorted set to the larger one when both have the same comparer, so the element of the larger one + // stays (ADR 0001, D14); addRange keeps the element of the sorted set that receives the elements + [] + member _.``addRange keeps the elements of the sorted set when the sequence is a larger sorted set with its comparer`` () = + let set = caseless.ofArray [| "apple" |] + let larger = caseless.ofArray [| "APPLE"; "banana" |] + + CollectionAssert.AreEqual ( + [| "apple"; "banana" |], + SortedSet.toArray (SortedSet.addRange larger set), + "SortedSet.addRange must keep the element of the sorted set that receives the elements" + ) + + CollectionAssert.AreEqual ( + [| "APPLE"; "banana" |], + SortedSet.toArray (SortedSet.union set larger), + "SortedSet.union must keep the element of the larger sorted set when both have the same comparer" + ) + + [] + member _.``addRange to an empty sorted set keeps its comparer for a sorted set with another comparer`` () = + // ImmutableSortedSet.Union answers an empty sorted set with the sorted set of its argument when the comparers are the + // same; with another comparer the result must still order with the comparer of the empty sorted set + let result = SortedSet.addRange (SortedSet.ofArray [| "apple"; "APPLE" |]) caseless.empty + + Assert.AreSame (ignoreCase, SortedSet.comparer result, "SortedSet.addRange must keep the comparer of the sorted set") + Assert.HasCount (1, result, "SortedSet.addRange must compare with the comparer of the sorted set") + + [] + member _.``remove removes the element that equals the value in the comparer`` () = + CollectionAssert.AreEqual ( + [| "Banana"; "cherry" |], + SortedSet.toArray (SortedSet.remove "APPLE" (fruits ())), + "SortedSet.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" |], + SortedSet.toArray (SortedSet.removeRange [| "APPLE"; "date" |] (fruits ())), + "SortedSet.removeRange must compare with the comparer of the sorted set" + ) + + [] + member _.``filter and partition keep the comparer`` (name : string) = + let struct (select, expected) = selections[name] + let result = select (fruits ()) + + Assert.AreSame (ignoreCase, SortedSet.comparer result, $"SortedSet.%s{name} must keep the comparer") + CollectionAssert.AreEqual (expected, SortedSet.toArray result, $"SortedSet.%s{name} must order with the comparer") + + [] + member _.``partition keeps comparing without case`` () = + Assert.IsTrue ( + SortedSet.contains "BANANA" (longFruits (fruits ())), + "SortedSet.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 (fruits ()) (SortedSet.ofArray [| "APPLE"; "date" |]) + + Assert.AreSame (ignoreCase, SortedSet.comparer result, $"SortedSet.%s{name} must keep the comparer of the first set") + + CollectionAssert.AreEqual ( + expected, + SortedSet.toArray result, + $"SortedSet.%s{name} must compare and order 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 documentation of IsProperSupersetOf says (ADR 0001, D14) + Assert.IsTrue ( + SortedSet.isProperSuperset (caseless.ofArray [| "apple"; "Banana" |]) (SortedSet.ofArray [| "apple"; "APPLE" |]), + "SortedSet.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 = fruits () + + Assert.IsTrue (SortedSet.isSubset apple set, "SortedSet.isSubset must compare without case") + Assert.IsTrue (SortedSet.isProperSubset apple set, "SortedSet.isProperSubset must compare without case") + Assert.IsTrue (SortedSet.isSuperset set apple, "SortedSet.isSuperset must compare without case") + Assert.IsTrue (SortedSet.isProperSuperset set apple, "SortedSet.isProperSuperset must compare without case") + Assert.IsTrue (SortedSet.overlaps apple set, "SortedSet.overlaps must compare without case") + + Assert.IsTrue ( + SortedSet.setEquals set (SortedSet.ofArray [| "APPLE"; "BANANA"; "CHERRY" |]), + "SortedSet.setEquals must compare without case" + ) + + Assert.IsFalse ( + SortedSet.isSubset (SortedSet.ofArray [| "APPLE" |]) set, + "SortedSet.isSubset must compare with the default comparer of the first set" + ) + + [] + member _.``intersectMany and unionMany keep the comparer of the first set`` () = + let sets = [| fruits (); SortedSet.ofArray [| "APPLE"; "date" |] |] + let intersection = SortedSet.intersectMany sets + let union = SortedSet.unionMany sets + + Assert.AreSame ( + ignoreCase, + SortedSet.comparer intersection, + "SortedSet.intersectMany must keep the comparer of the first set" + ) + + Assert.AreSame (ignoreCase, SortedSet.comparer union, "SortedSet.unionMany must keep the comparer of the first set") + + CollectionAssert.AreEqual ( + [| "APPLE" |], + SortedSet.toArray intersection, + "SortedSet.intersectMany must compare without case" + ) + + CollectionAssert.AreEqual ( + [| "apple"; "Banana"; "cherry"; "date" |], + SortedSet.toArray union, + "SortedSet.unionMany must compare without case" + ) + + [] + member _.``unionMany of no sets is empty with F# generic comparison`` () = + let union = SortedSet.unionMany Seq.empty> + Assert.IsEmpty (union, "SortedSet.unionMany of no sets must be empty") + + Assert.AreSame ( + defaultComparer, + SortedSet.comparer union, + "SortedSet.unionMany of no sets must compare with F# generic comparison" + ) + + [] + member _.``map, choose and partitionWith compare with F# generic comparison`` (name : string) = + let struct (transform, expected) = typeChanges[name] + let result = transform (fruits ()) + + Assert.AreSame (defaultComparer, SortedSet.comparer result, $"SortedSet.%s{name} must compare with F# generic comparison") + CollectionAssert.AreEqual (expected, SortedSet.toArray result, $"SortedSet.%s{name} must order ordinally") + + [] + 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, + SortedSet.add "APPLE" (SortedSet.map id (fruits ())), + "SortedSet.map must not keep the comparer of its input" + ) + + [] + member _.``map to another type orders the results as Set.map does`` () = + // The strings of the pairs differ in case, so the culture would order them differently + CollectionAssert.AreEqual ( + Set.toArray (Set.map snd (Set.ofArray pairs)), + SortedSet.toArray (SortedSet.map snd (SortedSet.ofArray pairs)), + "SortedSet.map must order the results as Set.map does" + ) + + // ---- Sharing ---- + + [)>] + member _.``ofSeq of a sorted set with the comparer returns it`` (input : int array) = + let set = SortedSet.ofArray input + Assert.AreSame (set, SortedSet.ofSeq set, "SortedSet.ofSeq of a sorted set with the default comparer must return it") + + let downward = SortedSet.forComparer descending + let ordered = downward.ofArray input + + Assert.AreSame ( + ordered, + downward.ofSeq ordered, + "CustomComparerSortedSet.ofSeq of a sorted set with the comparer must return it" + ) + + [] + member _.``add returns the input when it contains an equal element`` () = + let set = fruits () + + Assert.AreSame (set, SortedSet.add "APPLE" set, "SortedSet.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 = fruits () + Assert.AreSame (set, SortedSet.remove "date" set, "SortedSet.remove of a missing element must return the input") + + [] + member _.``clear returns an empty input`` () = + let set = caseless.empty + Assert.AreSame (set, SortedSet.clear set, "SortedSet.clear of an empty sorted set must return the input") + + // ---- Access ---- + + [] + member _.``tryGetValue returns the element that the sorted set holds`` () = + let stored = String ('x', 3) + let set = caseless.ofArray [| stored; "apple" |] + + match SortedSet.tryGetValue "XXX" set with + | ValueSome found -> Assert.AreSame (stored, found, "SortedSet.tryGetValue must return the instance of the sorted set") + | ValueNone -> Assert.Fail "SortedSet.tryGetValue must find the element that equals the value in the comparer" + + Assert.AreEqual( + ValueSome "apple", + SortedSet.tryGetValue "APPLE" set, + "SortedSet.tryGetValue must compare without case" + ) + + Assert.AreEqual( + ValueNone, + SortedSet.tryGetValue "date" set, + "SortedSet.tryGetValue must return nothing for a missing value" + ) + + [] + member _.``contains compares with the comparer of the sorted set`` () = + Assert.IsTrue (SortedSet.contains "BANANA" (fruits ()), "SortedSet.contains must compare without case") + + Assert.IsFalse ( + SortedSet.contains "BANANA" (SortedSet.ofArray [| "Banana" |]), + "SortedSet.contains must compare with F# generic comparison" + ) + + [] + member _.``item returns the elements in ascending order`` () = + let set = SortedSet.ofArray numbers + + CollectionAssert.AreEqual ( + Set.toArray (Set.ofArray numbers), + Array.init (SortedSet.count set) (fun index -> SortedSet.item index set), + "SortedSet.item must return the elements in ascending order" + ) + + [] + member _.``item throws ArgumentOutOfRangeException for an index out of range`` (index : int) = + let set = SortedSet.ofArray numbers + + let ex = + Assert.ThrowsExactly( + (fun () -> SortedSet.item index set |> ignore), + "SortedSet.item must throw what the indexer throws" + ) + + Assert.AreEqual("index", ex.ParamName, "SortedSet.item must name the index") + + [] + member _.``tryItem returns the element at the index or nothing`` (index : int, expected : int voption) = + Assert.AreEqual( + expected, + SortedSet.tryItem index (SortedSet.ofArray numbers), + "SortedSet.tryItem must return the element at the index or nothing" + ) + + [] + member _.``tryItem of an empty sorted set returns nothing`` () = + Assert.AreEqual( + ValueNone, + SortedSet.tryItem 0 SortedSet.empty, + "SortedSet.tryItem of an empty sorted set must return nothing" + ) + + // The negative results are the bitwise complements of the index of the next larger element: ~~~0, ~~~2 and ~~~3 + [] + member _.``indexOf returns the index or the complement of the index of the next larger element`` + (value : int, expected : int) + = + Assert.AreEqual( + expected, + SortedSet.indexOf value (SortedSet.ofArray [| 30; 10; 20 |]), + "SortedSet.indexOf must return the index or the complement of the index of the next larger element" + ) + + [] + member _.``indexOf compares with the comparer of the sorted set`` () = + Assert.AreEqual(1, SortedSet.indexOf "BANANA" (fruits ()), "SortedSet.indexOf must compare with the comparer") + + [] + member _.``minElement and maxElement return the smallest and the largest element as Set does`` () = + Assert.AreEqual( + Set.minElement (Set.ofArray numbers), + SortedSet.minElement (SortedSet.ofArray numbers), + "SortedSet.minElement of numbers" + ) + + Assert.AreEqual( + Set.maxElement (Set.ofArray numbers), + SortedSet.maxElement (SortedSet.ofArray numbers), + "SortedSet.maxElement of numbers" + ) + + Assert.AreEqual( + Set.minElement (Set.ofArray words), + SortedSet.minElement (SortedSet.ofArray words), + "SortedSet.minElement of strings" + ) + + Assert.AreEqual( + Set.maxElement (Set.ofArray words), + SortedSet.maxElement (SortedSet.ofArray words), + "SortedSet.maxElement of strings" + ) + + Assert.AreEqual( + Set.maxElement (Set.ofArray pairs), + SortedSet.maxElement (SortedSet.ofArray pairs), + "SortedSet.maxElement of tuples" + ) + + Assert.AreEqual(42, SortedSet.minElement (SortedSet.singleton 42), "SortedSet.minElement of one element") + Assert.AreEqual(42, SortedSet.maxElement (SortedSet.singleton 42), "SortedSet.maxElement of one element") + + [] + member _.``minElement and maxElement follow the comparer of the sorted set`` () = + let set = (SortedSet.forComparer descending).ofArray numbers + Assert.AreEqual(12, SortedSet.minElement set, "SortedSet.minElement must return the first element of the comparer") + Assert.AreEqual(-10, SortedSet.maxElement set, "SortedSet.maxElement must return the last element of the comparer") + + [] + member _.``minElement throws for an empty sorted set as Set does`` () = + ExceptionAssert.Same ( + SortedSetOps.paramName, + (fun () -> Set.minElement Set.empty |> ignore), + (fun () -> SortedSet.minElement SortedSet.empty |> ignore), + "SortedSet.minElement of an empty sorted set must throw" + ) + + [] + member _.``maxElement throws for an empty sorted set as Set does`` () = + ExceptionAssert.Same ( + SortedSetOps.paramName, + (fun () -> Set.maxElement Set.empty |> ignore), + (fun () -> SortedSet.maxElement SortedSet.empty |> ignore), + "SortedSet.maxElement of an empty sorted set must throw" + ) + + // ---- Null inputs ---- + + [] + member _.``functions reject a null sorted set`` (name : string) = + let struct (paramName, actual) = nullRejections[name] + + let ex = + Assert.ThrowsExactly(Action actual, $"SortedSet.%s{name} must reject a null sorted set") + + Assert.AreEqual(paramName, ex.ParamName, $"SortedSet.%s{name} must name the parameter of the sorted 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 ($"SortedSet.%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 objectComparer = SortedSet.forComparer (Comparer.Default :> IComparer) + let objects = objectComparer.ofArray [| ("b" :> obj); ("c" :> obj) |] + + CollectionAssert.AreEqual ( + [| ("a" :> obj); ("b" :> obj) |], + SortedSet.toArray (objectComparer.ofSeq widened), + "CustomComparerSortedSet.ofSeq must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + [| ("a" :> obj); ("b" :> obj); ("c" :> obj) |], + SortedSet.toArray (SortedSet.addRange widened objects), + "SortedSet.addRange must take the elements of a flat list of strings as objects" + ) + + CollectionAssert.AreEqual ( + [| ("c" :> obj) |], + SortedSet.toArray (SortedSet.removeRange widened objects), + "SortedSet.removeRange must take the elements of a flat list of strings as objects" + ) + + // The members of the struct reject null inputs in the creation contract + [] + member _.``ofBuilder rejects a null builder`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + SortedSet.ofBuilder (Unchecked.defaultof.Builder>) + |> ignore + ), + "SortedSet.ofBuilder must reject a null builder" + ) + + Assert.AreEqual("builder", ex.ParamName, "SortedSet.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 () -> SortedSet.forComparer nullComparer |> ignore), + "SortedSet.forComparer must reject a null comparer" + ) + + Assert.AreEqual("comparer", ex.ParamName, "SortedSet.forComparer must name the parameter comparer") + + [] + member _.``the constructor of the struct rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> CustomComparerSortedSet nullComparer |> ignore), + "The constructor of CustomComparerSortedSet must reject a null comparer" + ) + + Assert.AreEqual( + "comparer", + ex.ParamName, + "The constructor of CustomComparerSortedSet must name the parameter comparer" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTupleTests.fs new file mode 100644 index 00000000..40fdd167 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedSet/SortedSetTupleTests.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 `SortedSet` module of the `Tuple` namespace. +[] +type SortedSetTupleTests () = + + static let numbers = SortedSet.ofArray Inputs.Several + + static let expected = Set.ofArray Inputs.Several + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : SortedSet), (rejected : SortedSet) = + SortedSet.partition (fun item -> item > 0) numbers + let expectedAccepted, expectedRejected = Set.partition (fun item -> item > 0) expected + + CollectionAssert.AreEqual ( + Set.toArray expectedAccepted, + SortedSet.toArray accepted, + "Tuple.SortedSet.partition must return the accepted elements in the order of Set" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedRejected, + SortedSet.toArray rejected, + "Tuple.SortedSet.partition must return the rejected elements in the order of Set" + ) + + [] + member _.``partitionWith returns a reference tuple`` () = + let partitioner (item : int) = + if item > 0 then + Choice1Of2 item + else + Choice2Of2 (string item) + + let (first : SortedSet), (second : SortedSet) = SortedSet.partitionWith partitioner numbers + let expectedFirst, expectedSecond = Set.partitionWith partitioner expected + + CollectionAssert.AreEqual ( + Set.toArray expectedFirst, + SortedSet.toArray first, + "Tuple.SortedSet.partitionWith must return the first choices in the order of Set" + ) + + CollectionAssert.AreEqual ( + Set.toArray expectedSecond, + SortedSet.toArray second, + "Tuple.SortedSet.partitionWith must return the second choices in the order of Set" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : int voption = SortedSet.tryGetValue 8 numbers + Assert.AreEqual(ValueSome 8, found, "SortedSet.tryGetValue must resolve to the default module") From b8fb3da11a4cd36e103fdd7c4ec24e950ac19c3e Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Sun, 18 Oct 2026 13:56:51 +0200 Subject: [PATCH 3/4] docs: `SortedSet` in the changelog and the collection table Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 62 ++++++++++++++++++++++++++++++ README.md | 2 +- docsSrc/Explanations/Background.md | 2 +- docsSrc/index.md | 2 +- 4 files changed, 65 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e156a682..a9a5a32d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,68 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `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`. +* `SortedSet` has every function of the FSharp.Core `Set` module, with its semantics, argument order and exceptions, plus + `addRange`, `build`, `builder`, `choose`, `clear`, `comparer`, `forComparer`, `indexOf`, `item`, `ofBuilder`, + `ofReadOnlySpan`, `ofSpan`, `overlaps`, `removeRange`, `rev`, `setEquals`, `symmetricDifference`, `toBuilder`, + `tryGetValue`, `tryItem` and `update`. +* `SortedSet.addRange elements set` and `removeRange elements set` take the place of the `Union` and `Except` members that + take a sequence. The result keeps the comparer of the sorted set, and the input itself comes back when nothing changes. + `removeRange` wraps `Except`. `addRange` adds the elements one by one, so an element of the sequence that the comparer + finds equal to an element of the sorted set never replaces it, and of several equal elements of the sequence the first + is added. `Union` does not promise that: when the sequence is large against the sorted set, it refills the tree from an + unstable sort and can keep an element of the sequence instead. +* `SortedSet.ofReadOnlySpan` and `ofSpan` create a sorted set from a span and keep the first of the elements that the + comparer finds equal, where `ImmutableSortedSet.Create` with a span keeps an arbitrary one of them. `build` fills a + builder with an action, and `update` applies an action to a builder of a sorted set and returns its contents, so that + the in-place operations of the builder, such as `IntersectWith`, are one expression. +* `SortedSet.forComparer comparer` creates the `CustomComparerSortedSet` struct, whose members `build`, `builder`, `empty`, + `ofArray`, `ofList`, `ofReadOnlySpan`, `ofSeq`, `ofSpan` and `singleton` create sorted sets and builders that order + their elements with the comparer, as in `(SortedSet.forComparer StringComparer.OrdinalIgnoreCase).ofSeq names`. They + replace the BCL members `Create(comparer, ...)`, `CreateRange(comparer, items)`, `ToImmutableSortedSet(items, comparer)` + and `CreateBuilder(comparer)`. `ofSeq` also gives an existing sorted set the comparer, as `WithComparer` does: the + sorted set itself comes back for the same comparer instance, and an equal but distinct instance rebuilds it, keeping the + first of the elements that the comparer finds equal. A `null` comparer throws `ArgumentNullException`. + The element type needs no F# comparison. The default value of the struct orders with `Comparer<'T>.Default`, as the BCL + does for a `null` comparer, so `WithComparer(null)` is `ofSeq` of the default value, where `SortedSet.ofSeq set` orders + with F# generic comparison. +* `SortedSet.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. Sorted 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 sorted set and have no function. +* `add`, `addRange`, `remove`, `removeRange`, `clear`, `update`, `filter` and `partition` of `SortedSet` keep the comparer + of their input, and the set algebra keeps the comparer of the first sorted set. Every function throws + `ArgumentNullException` for a `null` sorted set, sequence or array, and for a default flat list that is given as a + sequence; `yield!` and `for` of `sortedSet { }` do the same. `unionMany` of an empty sequence returns an empty sorted + 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 functions of `SortedSet` that create a sorted set without a comparer, `builder`, `build`, `unionMany` of no sets, + `map`, `choose`, `partitionWith` and `sortedSet { }` order with F# generic comparison, as `Set` does, where the BCL + orders with `Comparer<'T>.Default`. A sorted set thus enumerates its elements in the order of the F# set of the same + elements: strings, and tuples and other values that contain strings, compare ordinally instead of in the order of the + current culture. `SortedSet.forComparer Comparer<'T>.Default` orders as the BCL does. +* The set algebra and the relations of `SortedSet` take sorted sets, as the `Set` functions do. The BCL members that take + a sequence map as follows: `Union` and `Except` are `addRange` and `removeRange`, `Overlaps` and `IsSupersetOf` are + `Seq.exists` and `Seq.forall` with `contains`, and the others take `SortedSet.ofSeq elements`, or + `(SortedSet.forComparer (SortedSet.comparer set)).ofSeq elements` to give the elements the comparer of the first sorted + set. +* The element that stays when the comparer finds two elements equal follows the BCL: `union` keeps the element of the + first sorted set, or of the larger one when both have the same comparer, `intersect` keeps the elements of the second + one, and the creation functions keep the first element of their input, where `CreateRange` and `Create` with a span keep + any of them. When the second sorted set of `union` has another comparer and is much larger, the BCL refills the tree + from an unstable sort and either element can stay; only `addRange` keeps the element of the sorted set in every case. + `isProperSuperset` is the exception: it counts the elements of the second sorted set that the comparer of the first one + finds equal once, as the documentation of `IsProperSupersetOf` says, where the member counts each of them. +* `SortedSet.minElement` and `maxElement` take the place of the properties `Min` and `Max`: they read the first and the + last element in O(log n) and throw `ArgumentException` for an empty sorted set, as `Set.minElement` and + `Set.maxElement` do, where the properties return `default`. `tryItem 0` and `tryItem (count - 1)` return a `voption` + instead. `rev` is `Reverse`, `item` is the indexer and throws `ArgumentOutOfRangeException` like it, and `indexOf` is + `IndexOf`, with the bitwise complement for a missing element. `ItemRef` has no function, because a curried function + cannot return a `byref`. +* The `FSharp.Collections.Immutable.Option.SortedSet` and `FSharp.Collections.Immutable.Tuple.SortedSet` modules repeat + `choose`, `tryGetValue` and `tryItem` with options, and `partition` and `partitionWith` with reference tuples. +* `sortedSet { }` builds a sorted set, and `SortedSetCE` created with a comparer builds sorted sets that order their + elements with it; a `null` comparer throws `ArgumentNullException`. ### Changed diff --git a/README.md b/README.md index 555b4c97..268d4ec1 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | -| `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `SortedSet` | `ImmutableSortedSet<'T>` | `sortedSet { }` | | `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index 88b0f21a..417ddaae 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -33,7 +33,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | -| `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `SortedSet` | `ImmutableSortedSet<'T>` | `sortedSet { }` | | `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/index.md b/docsSrc/index.md index 7541de20..61462769 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -15,7 +15,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | – | | `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | | `HashSet` | `ImmutableHashSet<'T>` | `hashSet { }` | -| `SortedSet` | `ImmutableSortedSet<'T>` | – | +| `SortedSet` | `ImmutableSortedSet<'T>` | `sortedSet { }` | | `ISet` | `IImmutableSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | From 045901196b350715487c30e18ee4f99bfd578bcc Mon Sep 17 00:00:00 2001 From: XperiAndri Date: Wed, 21 Oct 2026 04:01:17 +0200 Subject: [PATCH 4/4] refactor: the `Tuple` and `Option` modules of `SortedSet` call the default modules by their short names Each of these files opens `FSharp.Collections.Immutable`, and inside `FSharp.Collections.Immutable.Tuple.SortedSet` the name `SortedSet` reaches the default module, also for the functions that both modules define, so the full namespace in front of it added nothing. The compiled code is the same: the opcodes and the called methods of every method of both builds match one to one. Co-Authored-By: Claude Sonnet 5.5 --- .../SortedSet/SortedSetOption.fs | 10 +++------- .../SortedSet/SortedSetTuple.fs | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs index 8243f4bd..169d175a 100644 --- a/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetOption.fs @@ -8,17 +8,13 @@ open FSharp.Collections.Immutable // ---- Access ---- [] -let tryGetValue (value : 'T) (set : SortedSet<'T>) = - FSharp.Collections.Immutable.SortedSet.tryGetValue value set - |> ValueOption.toOption +let tryGetValue (value : 'T) (set : SortedSet<'T>) = SortedSet.tryGetValue value set |> ValueOption.toOption [] -let tryItem (index : int) (set : SortedSet<'T>) = - FSharp.Collections.Immutable.SortedSet.tryItem index set - |> ValueOption.toOption +let tryItem (index : int) (set : SortedSet<'T>) = SortedSet.tryItem index set |> ValueOption.toOption // ---- Transformation ---- [] let choose (chooser : 'T -> 'U option) (set : SortedSet<'T>) = - FSharp.Collections.Immutable.SortedSet.choose (fun item -> chooser item |> ValueOption.ofOption) set + SortedSet.choose (fun item -> chooser item |> ValueOption.ofOption) set diff --git a/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs index 75cc7cad..9694af4c 100644 --- a/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs +++ b/src/FSharp.Collections.Immutable/SortedSet/SortedSetTuple.fs @@ -9,10 +9,10 @@ open FSharp.Collections.Immutable [] let partition (predicate : 'T -> bool) (set : SortedSet<'T>) = - let struct (accepted, rejected) = FSharp.Collections.Immutable.SortedSet.partition predicate set + let struct (accepted, rejected) = SortedSet.partition predicate set accepted, rejected [] let partitionWith (partitioner : 'T -> Choice<'T1, 'T2>) (set : SortedSet<'T>) = - let struct (first, second) = FSharp.Collections.Immutable.SortedSet.partitionWith partitioner set + let struct (first, second) = SortedSet.partitionWith partitioner set first, second