diff --git a/CHANGELOG.md b/CHANGELOG.md index 7872abdb..a6dbd57c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 modules that repeat the functions that use value options or return struct tuples with options and reference tuples. * `hashMap { }` builds a hash map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; `HashMapCE` takes a key comparer, and a `null` one throws `ArgumentNullException`. +* `SortedMap` has every function of the FSharp.Core `Map` module that applies to a sorted map, with its semantics and + exceptions, `minKeyValue` and `maxKeyValue` included, except `add`, which is the `Add` of + `ImmutableSortedDictionary`; plus `choose` and `singleton`, the builder functions `build`, `builder`, `ofBuilder`, + `toBuilder` and `update`, and the wrappers of the `ImmutableSortedDictionary` members `add`, `addRange`, `clear`, + `containsBinding`, `containsValue`, `keyComparer`, `ofSeqBy`, `removeRange`, `setItem`, `setItems`, `tryGetKey` and + `valueComparer`, which keep the semantics of the BCL. `maxKeyValue` visits every binding, because + `ImmutableSortedDictionary` gives no other access to its largest key. +* `SortedMap.containsBinding` tells whether a key is bound to a value, comparing the value with the value comparer of the + map, as `Contains` of a `KeyValuePair` does. `tryFind` followed by `=` compares with F# equality, which can differ from + the value comparer. A `null` key throws `ArgumentNullException`. +* `SortedMap.build` creates a sorted map with a builder that an action fills, `SortedMap.update` changes a sorted map with + a builder, and the member `build` of `CustomComparersSortedMap` does the first with its comparers. +* `SortedMap.ofArray`, `SortedMap.ofList` and `SortedMap.ofSeq` take the bindings as `KeyValuePair`s, reference tuples or + struct tuples: they are static members of `ImmutableSortedDictionary<'Key, 'Value>` in the auto-opened + `SortedMapExtensions` module, one overload per shape, and every shape keeps the last binding of a repeated key, as + `Map.ofSeq` does, unless the value comparer finds the two values equal. They take a dictionary, an F# `Map` or another + map too. C# does not see them as members of `ImmutableSortedDictionary`; it calls the public static methods + `SortedMapExtensions.OfArray`, `OfList` and `OfSeq`, or the members of the struct below. A reflection over the + `SortedMap` module does not list them. To reject another value of a repeated key, as + `ImmutableSortedDictionary.CreateRange` does, add the bindings to an empty map: + `SortedMap.empty |> SortedMap.addRange pairs`. +* `SortedMap.forComparers` and `SortedMap.forKeyComparer` return a `CustomComparersSortedMap<'Key, 'Value>` struct whose + members `build`, `builder`, `empty`, `ofArray`, `ofList`, `ofSeq`, `ofSeqBy` and `singleton` create sorted maps with its + comparers: `(SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq pairs`. A `null` comparer throws + `ArgumentNullException`. +* `ofSeq` of that struct also gives an existing sorted map other comparers, as `WithComparers` does, with three + differences. It takes O(1) only when the key comparer of the map is the instance of the struct, and rebuilds the map + otherwise; a sorted map that the module creates orders its keys with F# generic comparison, so pass + `SortedMap.keyComparer table` to change only its value comparer in O(1). Keys that the new comparer makes equal merge, + the later binding winning, where `WithComparers` throws. And `forKeyComparer` resets the value comparer, where + `WithComparers` keeps it; to keep it, pass the value comparer of the map: + `SortedMap.forComparers keyComparer (SortedMap.valueComparer table)`. +* `SortedMap.addRange` and `SortedMap.setItems` take `KeyValuePair`s; the nested modules `SortedMap.Tuple` and + `SortedMap.ValueTuple` take reference and struct tuples: `SortedMap.Tuple.addRange pairs table`. +* The single-key idioms of the BCL are functions of the module: `tryFind` is `TryGetValue`, `tryFind` followed by + `ValueOption.defaultValue` is `GetValueOrDefault`, and `change` with a function that keeps an existing value adds a + binding only when the key has none. `ImmutableInterlocked` has no member for a sorted dictionary, but its `Update` + replaces a field atomically with the result of a function of the sorted map, such as `SortedMap.change`. + `ToImmutableSortedDictionary` with an element selector has no function; to reject a repeated key like the BCL does, add + the mapped pairs to an empty map, + `SortedMap.empty |> SortedMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x)))`, and + `SortedMap.ofSeq` of the mapped pairs lets the last binding win. Maps have no builder module: call `Add`, `AddRange`, + `Remove`, `RemoveRange`, the indexer and the other members on the builder. +* The functions of `SortedMap` that create a sorted map without a key comparer (`singleton`, `ofSeqBy`, `build`, `builder`, + `ofArray`, `ofList`, `ofSeq` and `sortedMap { }`) order its keys with F# generic comparison, as `Map` does, so the keys + need `comparison`, and a sorted map of strings enumerates them in ordinal order, where `Comparer<'Key>.Default` orders + them by the rules of the current culture. +* The transformations of `SortedMap` keep the key comparer of the map, and the value comparer when the type of the values + does not change; `map` and `choose` compare the new values with the default comparer. +* `tryFind`, `tryFindKey`, `tryGetKey` and `tryPick` of `SortedMap` return a `voption`, `tryFindKey` returns the key, + `pick` and `tryPick` take choosers that return a `voption`, and `partition`, `toArray`, `toList`, `toSeq`, + `minKeyValue` and `maxKeyValue` return struct tuples. `find`, `findKey` and `pick` throw the `KeyNotFoundException` of + `Map`, and every function throws `ArgumentNullException` for a `null` map, sequence or builder, and for a default + `FlatList` given as a sequence, which would fail only when it is enumerated. +* `SortedMap` keeps the convention of `ImmutableSortedDictionary` where it differs from `Map`: a new value of a key that + the value comparer finds equal to the current one leaves the binding as it is, its key included, in `add`, `change`, + `setItem`, `setItems`, `addRange`, the creation functions and members, and `sortedMap { }`. `add` is the `Add` of + `ImmutableSortedDictionary`: it throws `ArgumentException` for a key that is bound to a value that the value comparer + finds different, and returns the input itself for the same binding; `setItem` replaces the value. +* `SortedMap.toSeq` and `SortedMap.ofSeq` replace `Seq.ofSortedMap` and `Seq.toSortedMap` of the former `Seq` module of + conversions between the collections. `toSeq` yields struct tuples instead of `KeyValuePair`s, and a sorted map is + itself a `seq>`, so `Seq.toArray table` gives the old shape. +* The `FSharp.Collections.Immutable.Option` and `FSharp.Collections.Immutable.Tuple` namespaces have `SortedMap` modules + that repeat the functions that use value options or return struct tuples with options and reference tuples. +* `sortedMap { }` builds a sorted map from `yield struct (key, value)` and `yield KeyValuePair (key, value)`; + `SortedMapCE` takes a key comparer, and a `null` one throws `ArgumentNullException`, as do `yield!` of a `null` + sequence, array or sorted map, `for` over a `null` sequence, and a default `FlatList` given as a sequence to `yield!` + or `for`. ### Changed @@ -109,6 +177,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `HashMap.ofSeq` throws `ArgumentNullException` for a default flat list given as a sequence, where it threw `InvalidOperationException`. * `HashMap.filter` returns the bindings that satisfy the predicate, where it returned all the bindings of the hash map. +* `SortedMap.empty` orders its keys with F# generic comparison, as `Map.empty` does, where it used + `Comparer<'Key>.Default`, which orders strings by the rules of the current culture: a sorted map of strings now + enumerates them in ordinal order. It requires `'Key : comparison`, where it accepted any key type; for keys without + comparison, or for another order, use `(SortedMap.forKeyComparer comparer).empty`. C# callers see the compiled name + `Empty`, where it was `empty`. ### Removed @@ -131,6 +204,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `HashMap.check`: no replacement is needed, because every function of the module throws `ArgumentNullException` for a `null` hash map. * The `HashMapBuilder<'Key, 'Value>` type abbreviation: use `HashMap<'Key, 'Value>.Builder`. +* The `SortedMapBuilder<'Key, 'Value>` abbreviation: use `SortedMap<'Key, 'Value>.Builder`. ## [1.0.0] - 2020-02-23 diff --git a/README.md b/README.md index 9c1a830c..03255db6 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Please also join the [F# Open Source Group](http://fsharp.github.com) | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | -| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | `sortedMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/Explanations/Background.md b/docsSrc/Explanations/Background.md index d5cfbb4a..fdd0baa7 100644 --- a/docsSrc/Explanations/Background.md +++ b/docsSrc/Explanations/Background.md @@ -32,7 +32,7 @@ While F# has its own built-in immutable collections, System.Collections.Immutabl | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | -| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | `sortedMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/docsSrc/index.md b/docsSrc/index.md index 3b8f9b32..29a8e160 100644 --- a/docsSrc/index.md +++ b/docsSrc/index.md @@ -14,7 +14,7 @@ F# bindings for [System.Collections.Immutable](https://learn.microsoft.com/en-us | `Queue` | `ImmutableQueue<'T>` | – | | `HashMap` | `ImmutableDictionary<'Key, 'Value>` | `hashMap { }` | | `IMap` | `IImmutableDictionary<'Key, 'Value>` | – | -| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | – | +| `SortedMap` | `ImmutableSortedDictionary<'Key, 'Value>` | `sortedMap { }` | | `HashSet` | `ImmutableHashSet<'T>` | – | | `SortedSet` | `ImmutableSortedSet<'T>` | – | | `IIndexedSeq` | `IReadOnlyList<'T>` | – | diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fs b/src/FSharp.Collections.Immutable/ComputationExpressions.fs index 5a17dfba..21672373 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fs +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fs @@ -7,3 +7,6 @@ let flatList = FlatListCE () [] let hashMap<'Key when 'Key : not null> = HashMapCE<'Key>() + +[] +let sortedMap<'Key when 'Key : not null and 'Key : comparison> = SortedMapCE<'Key>() diff --git a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi index 7cfa666f..a7555537 100644 --- a/src/FSharp.Collections.Immutable/ComputationExpressions.fsi +++ b/src/FSharp.Collections.Immutable/ComputationExpressions.fsi @@ -41,3 +41,26 @@ val flatList : FlatListCE /// [] val hashMap<'Key when 'Key : not null> : HashMapCE<'Key> + +/// +/// Builds a sorted map whose keys are ordered by F# generic comparison. +/// +/// +/// A later binding of a key replaces an earlier one, unless the default comparer of the values finds the two values +/// equal: then the earlier binding stays, its key included (ADR 0001, D14). The map enumerates its bindings in the +/// ascending order of the keys, as an F# map does. The instance is generic in the type of the keys, which each expression +/// infers; takes a key comparer. +/// +/// +/// +/// let squares = sortedMap { +/// for i in 3..-1..1 do +/// yield struct (i, i * i) +/// yield KeyValuePair (10, 100) +/// yield struct (1, -1) +/// } +/// +/// Evaluates to a sorted map of 1 to -1, 2 to 4, 3 to 9 and 10 to 100, in this order. +/// +[] +val sortedMap<'Key when 'Key : not null and 'Key : comparison> : SortedMapCE<'Key> diff --git a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj index 2ed46392..d5f2b4f7 100644 --- a/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj +++ b/src/FSharp.Collections.Immutable/FSharp.Collections.Immutable.fsproj @@ -72,11 +72,31 @@ + + + SortedMap.fsi + + + SortedMap.fs + + + + + + + + + SortedMap.fsi + + + SortedMap.fs + + + - diff --git a/src/FSharp.Collections.Immutable/Maps.fs b/src/FSharp.Collections.Immutable/Maps.fs deleted file mode 100644 index efbec178..00000000 --- a/src/FSharp.Collections.Immutable/Maps.fs +++ /dev/null @@ -1,178 +0,0 @@ -namespace FSharp.Collections.Immutable - -open System.Collections.Generic - -type SortedMap<'Key, 'Value when 'Key : not null> = System.Collections.Immutable.ImmutableSortedDictionary<'Key, 'Value> - -type SortedMapBuilder<'Key, 'Value when 'Key : not null> = SortedMap<'Key, 'Value>.Builder - -[] -module SortedMap = - - type internal SortedMapFactory = System.Collections.Immutable.ImmutableSortedDictionary - - let inline check (sortedMap : SortedMap<_, _>) = checkNotNull (nameof sortedMap) sortedMap - - ////////// Creating ////////// - - let inline empty<'Key, 'Value when 'Key : not null> = SortedMapFactory.Create<'Key, 'Value>() - let inline singleton item = SortedMapFactory.Create (item) - - let inline ofSeq source = SortedMapFactory.CreateRange (source) - - let inline ofSeqWith getKey source = - source - |> Seq.map (fun i -> KeyValuePair (getKey i, i)) - |> SortedMapFactory.CreateRange - - let inline ofSeqGroupBy getKey source = - source - |> Seq.groupBy getKey - |> Seq.map (fun (key, value) -> KeyValuePair (key, value)) - - let inline ofArray (source : _ array) = SortedMapFactory.CreateRange (source) - - let inline toSeq (map : SortedMap<_, _>) = map :> seq<_> - - let inline toArray (map : SortedMap<_, _>) = - check map - Seq.toArray map - - ////////// Building ////////// - - let inline builder () = SortedMapFactory.CreateBuilder () - let inline builderWithKeyComparer comparer = SortedMapFactory.CreateBuilder (comparer) - let inline builderWithComparers keyComparer valueComparer = SortedMapFactory.CreateBuilder (keyComparer, valueComparer) - - let inline ofBuilder (sortedMapBuilder : SortedMapBuilder<_, _>) = - checkNotNull (nameof sortedMapBuilder) sortedMapBuilder - sortedMapBuilder.ToImmutable () - - let inline toBuilder map : SortedMapBuilder<_, _> = - check map - map.ToBuilder () - - let inline ofKeyComparer<'Key, 'Value when 'Key : not null> comparer = SortedMapFactory.Create<'Key, 'Value>(comparer) - let inline ofComparers<'Key, 'Value when 'Key : not null> keyComparer valueComparer = - SortedMapFactory.Create<'Key, 'Value>(keyComparer, valueComparer) - - - let inline isEmpty map = - check map - map.IsEmpty - - let inline length map = - check map - map.Count - - let inline keyComparer map = - check map - map.KeyComparer - - let inline valueComparer map = - check map - map.ValueComparer - - let inline containsKey key map = - check map - map.ContainsKey key - - let inline find key map = - check map - map.[key] - - let inline tryFind key map = - check map - - match map.TryGetValue (key) with - | true, value -> Some value - | false, _ -> None - - let inline vTryFind key map = - check map - - match map.TryGetValue (key) with - | true, value -> ValueSome value - | false, _ -> ValueNone - - let inline pick chooser map = - check map - map |> Seq.pick (fun kvp -> chooser kvp.Key kvp.Value) - - let inline tryPick chooser map = - check map - map |> Seq.tryPick (fun kvp -> chooser kvp.Key kvp.Value) - - let inline vTryPick chooser map = - check map - - match map |> Seq.tryPick (fun kvp -> chooser kvp.Key kvp.Value) with - | Some value -> ValueSome value - | None -> ValueNone - - let inline iter action (map : SortedMap<_, _>) = - check map - map |> Seq.iter (fun kvp -> action kvp.Key kvp.Value) - - let inline exists predicate map = - check map - map |> Seq.exists (fun kvp -> predicate kvp.Key kvp.Value) - - let inline add key value map : SortedMap<_, _> = - check map - map.Add (key, value) - - let inline append map pairs : SortedMap<_, _> = - check map - checkNotNull (nameof pairs) pairs - map.AddRange pairs - - let inline remove key map : SortedMap<_, _> = - check map - map.Remove key - - let inline except keys map : SortedMap<_, _> = - check map - map.RemoveRange keys - - let inline clear map : SortedMap<_, _> = - check map - map.Clear () - - let inline findKey predicate map = - check map - - match (map |> Seq.tryFind (fun kvp -> predicate kvp.Key kvp.Value)) with - | Some value -> value.Key - | None -> raise (new KeyNotFoundException ()) - - let inline tryFindKey predicate map = - check map - map |> Seq.tryFind (fun kvp -> predicate kvp.Key kvp.Value) - - let inline vTryFindKey predicate map = - check map - - match (map |> Seq.tryFind (fun kvp -> predicate kvp.Key kvp.Value)) with - | Some value -> ValueSome value.Key - | None -> ValueNone - - let inline filter predicate map = - map - |> Seq.filter (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - - let inline forall predicate map = - map - |> Seq.forall (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - - let inline map mapping (map' : SortedMap<_, _>) = - map' - |> Seq.map (fun (kvp : KeyValuePair<_, _>) -> mapping kvp.Key kvp.Value) - |> ofSeq - - let inline where predicate map = - map - |> Seq.where (fun (kvp : KeyValuePair<_, _>) -> predicate kvp.Key kvp.Value) - |> empty.AddRange diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fs new file mode 100644 index 00000000..a78fab69 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fs @@ -0,0 +1,129 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable + +// The struct holds the comparers and creates sorted maps with them (ADR 0001, D12). A struct cannot run code in a primary +// constructor, so the constructors that check the comparers are explicit. Every struct also has a default value that no +// constructor checks; its fields are null, and the properties give it the comparers that the BCL takes for null ones, +// which the members use. The struct cannot require F# comparison of the keys, because a custom comparer also serves keys +// without it, so its default value cannot fall back to F# generic comparison as the functions of the module do (D5). +[] +type CustomComparersSortedMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IComparer<'Key> | null + val private valueComparer : IEqualityComparer<'Value> | null + + new (keyComparer : IComparer<'Key>, valueComparer : IEqualityComparer<'Value>) + = + checkNotNull (nameof keyComparer) keyComparer + checkNotNull (nameof valueComparer) valueComparer + { keyComparer = keyComparer; valueComparer = valueComparer } + + new (keyComparer : IComparer<'Key>) + = + checkNotNull (nameof keyComparer) keyComparer + { keyComparer = keyComparer; valueComparer = EqualityComparer<'Value>.Default } + + /// Sets the bindings, split into keys and values, one by one in a builder with the comparers, so that a later binding + /// of a key replaces an earlier one, as in `Map.ofSeq`, unless the value comparer finds the two values equal: the + /// indexer of the builder then keeps the earlier binding, its key included, as .NET does (D14). Every shape of the + /// bindings creates its maps here (D13). + member inline private this.Build ([] split : 'Binding -> struct ('Key * 'Value), elements : seq<'Binding>) = + let builder = this.builder () + Dictionaries.setAll split builder elements + builder.ToImmutable () + + // ---- Creation ---- + + [] + member this.empty : SortedMap<'Key, 'Value> = + ImmutableSortedDictionary.Create<'Key, 'Value>(this.KeyComparer, this.ValueComparer) + + [] + member this.ofArray (elements : KeyValuePair<'Key, 'Value> array) = + checkNotNull (nameof elements) elements + this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofArray (elements : ('Key * 'Value) array) = + checkNotNull (nameof elements) elements + this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofArray (elements : struct ('Key * 'Value) array) = + checkNotNull (nameof elements) elements + this.Build (id, elements) + + [] + member this.ofList (elements : KeyValuePair<'Key, 'Value> list) = this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofList (elements : ('Key * 'Value) list) = this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofList (elements : struct ('Key * 'Value) list) = this.Build (id, elements) + + [] + member this.ofSeq (elements : seq>) = + // A default flat list given as a sequence is as good as a null one: it fails when it is enumerated, and the + // exception would name a parameter of the BCL (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof elements) elements + + match elements with + // The keys of a sorted map stay distinct and in order in its own key comparer, so WithComparers cannot meet a + // collision and returns the map itself or the map with the value comparer, without a copy; another key comparer + // can make keys equal, and WithComparers would reject them where a later binding has to win + | :? ImmutableSortedDictionary<'Key, 'Value> as table when obj.ReferenceEquals (table.KeyComparer, this.KeyComparer) -> + table.WithComparers (this.KeyComparer, this.ValueComparer) + | _ -> this.Build (Dictionaries.splitKeyValuePair, elements) + + [] + member this.ofSeq (elements : seq<'Key * 'Value>) = + checkNotNullOrDefaultSeq (nameof elements) elements + this.Build (Dictionaries.splitTuple, elements) + + [] + member this.ofSeq (elements : seq) = + checkNotNullOrDefaultSeq (nameof elements) elements + this.Build (id, elements) + + [] + member this.ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : SortedMap<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof source) source + + ImmutableSortedDictionary.ToImmutableSortedDictionary ( + source, + Func<'Value, 'Key> projection, + Func<'Value, 'Value> id, + this.KeyComparer, + this.ValueComparer + ) + + [] + member this.singleton (key : 'Key) (value : 'Value) : SortedMap<'Key, 'Value> = this.empty.Add (key, value) + + // ---- Builder ---- + + [] + member this.build (action : SortedMap<'Key, 'Value>.Builder -> unit) : SortedMap<'Key, 'Value> = + let builder = this.builder () + action builder + builder.ToImmutable () + + [] + member this.builder () : SortedMap<'Key, 'Value>.Builder = + ImmutableSortedDictionary.CreateBuilder<'Key, 'Value>(this.KeyComparer, this.ValueComparer) + + // ---- Properties ---- + + member this.KeyComparer : IComparer<'Key> = + match this.keyComparer with + | null -> Comparer<'Key>.Default + | comparer -> comparer + + member this.ValueComparer : IEqualityComparer<'Value> = + match this.valueComparer with + | null -> EqualityComparer<'Value>.Default + | comparer -> comparer diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fsi new file mode 100644 index 00000000..f4c0f3a5 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.CustomComparers.fsi @@ -0,0 +1,308 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Generic + +/// +/// The comparer that orders the keys of sorted maps and the equality comparer of their values, with the creation functions +/// of that create sorted maps with them (ADR 0001, D12). +/// +/// +/// +/// +/// and +/// create the struct. Its members have the names of the creation functions of the module and the comparers of the struct. +/// +/// +/// ofArray, ofList and ofSeq take the bindings as key/value pairs, reference tuples or struct +/// tuples (ADR 0001, D13). A later binding of a key replaces an earlier one, as in +/// , +/// unless the value comparer finds the two values equal: then the earlier binding stays, its key included, as in the +/// builder of (ADR 0001, D14). A repeated key never +/// throws, where +/// rejects another value of it: to reject it, add the bindings to an empty map, +/// (SortedMap.forComparers keyComparer valueComparer).empty |> SortedMap.addRange pairs. +/// +/// +/// ofSeq also takes an existing map, because a map is a sequence of key/value pairs, and gives its bindings the +/// comparers of the struct. This is how to change the comparers of a map, with three differences from +/// : +/// the cost, the keys that become equal and the value comparer. The remarks of the ofSeq member describe them. +/// +/// +/// The struct only holds the comparers, so creating it allocates nothing. Its default value holds no comparers and stands +/// for the comparers that takes for missing ones +/// (ADR 0001, D14): for the keys, which orders strings by the +/// rules of the current culture, and for the values. +/// The struct cannot fall back to F# generic comparison, which the functions of the module use, because it serves keys +/// without F# comparison too. +/// +/// +/// +/// +/// let table = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "two", 2; "one", 1; "ONE", 3 ] +/// +/// Evaluates to a sorted map whose keys ignore the case, with the bindings of "ONE" to 3 and "two" to 2, in this order. +/// +/// +/// +/// let ignoreCase = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table +/// let keepValues = (SortedMap.forComparers StringComparer.OrdinalIgnoreCase (SortedMap.valueComparer table)).ofSeq table +/// +/// Both evaluate to a sorted map whose keys ignore the case, in the order of that comparer: the first compares the values +/// with the default comparer, the second keeps the value comparer of table. +/// +[] +type CustomComparersSortedMap<'Key, 'Value when 'Key : not null> = + + val private keyComparer : IComparer<'Key> | null + val private valueComparer : IEqualityComparer<'Value> | null + + /// + /// Holds the comparer that orders the keys and the equality comparer of the values. + /// + /// The comparer that orders the keys. + /// The equality comparer of the values. + /// + /// Thrown when or is . + /// + new : keyComparer : IComparer<'Key> * valueComparer : IEqualityComparer<'Value> -> CustomComparersSortedMap<'Key, 'Value> + + /// + /// Holds the comparer that orders the keys, and the default comparer of the values. + /// + /// The comparer that orders the keys. + /// Thrown when is . + /// + /// The values compare with . + /// + new : keyComparer : IComparer<'Key> -> CustomComparersSortedMap<'Key, 'Value> + + // ---- Creation ---- + + /// + /// The empty sorted map with the comparers. + /// + /// + /// O(1). The property wraps + /// . + /// + [] + member empty : SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The sorted map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : KeyValuePair<'Key, 'Value> array -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The sorted map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : ('Key * 'Value) array -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The sorted map of the bindings. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofArray : elements : struct ('Key * 'Value) array -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The sorted map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : KeyValuePair<'Key, 'Value> list -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The sorted map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : ('Key * 'Value) list -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The sorted map of the bindings. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofList : elements : struct ('Key * 'Value) list -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The sorted map of the bindings. A sorted map with the key comparer of the struct comes back itself when it also has + /// the value comparer, and with the value comparer otherwise. + /// + /// Thrown when is or a default flat list. + /// + /// + /// O(n log n); O(1) for a sorted map whose key comparer is the instance that the struct holds, whose keys stay distinct + /// and in order, through + /// . + /// A sorted map with another key comparer is rebuilt, even when the comparer is equivalent. The sorted maps that the + /// module creates order their keys with F# generic comparison, so to give one of them another value comparer in O(1), + /// pass its own key comparer: (SortedMap.forComparers (SortedMap.keyComparer table) valueComparer).ofSeq table. + /// + /// + /// A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: then the + /// earlier binding stays, its key included (ADR 0001, D14). Keys that only the comparer of the struct considers equal + /// merge by the same rule, in the order of enumeration of the map, where WithComparers throws + /// for another value of them. + /// + /// + /// The value comparer of the struct replaces the one of the map. A struct from + /// + /// compares the values with the default comparer, where WithComparers with a key comparer keeps the value + /// comparer of the map: SortedMap.forComparers keyComparer (SortedMap.valueComparer table) keeps it. + /// + /// + [] + member ofSeq : elements : seq> -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The sorted map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq<'Key * 'Value> -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The sorted map of the bindings. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the value comparer finds the two values equal: + /// then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + member ofSeq : elements : seq -> SortedMap<'Key, 'Value> + + /// + /// Creates a sorted map with the comparers of the elements of a sequence under the keys that a projection returns for + /// them. + /// + /// The function that returns the key of an element. + /// The input sequence, whose elements become the values. + /// The sorted map of the elements under their keys. + /// Thrown when is or a default flat list. + /// + /// Thrown when two elements have equal keys and are not equal in the value comparer. + /// + /// + /// O(n log n). The member wraps + /// , + /// which rejects a repeated key with another element and accepts one with an element that the value comparer finds + /// equal: the first element stays, its key included (ADR 0001, D14). The value of a binding is the element itself; + /// with an element selector, (SortedMap.forComparers keyComparer valueComparer).empty |> SortedMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x))) + /// rejects a repeated key like ToImmutableSortedDictionary does, and ofSeq of the mapped pairs lets the + /// last binding win. + /// + [] + member ofSeqBy : projection : ('Value -> 'Key) -> source : seq<'Value> -> SortedMap<'Key, 'Value> + + /// Creates a sorted map of one binding with the comparers. + /// The key. + /// The value. + /// The sorted map of the binding. + /// O(1). + [] + member singleton : key : 'Key -> value : 'Value -> SortedMap<'Key, 'Value> + + // ---- Builder ---- + + /// + /// Creates a sorted map with the comparers with a builder that an action fills. + /// + /// The action that sets the bindings in the builder, which has the comparers. + /// The sorted map of the bindings that the action set. + /// + /// O(1) apart from the cost of the action, which sets each binding of the builder in O(log n), and from freezing the + /// nodes that the action changed: the sorted map shares the nodes of the builder instead of copying the entries. The + /// builder is the one that + /// creates, and the member returns it with + /// . The indexer of the + /// builder keeps the earlier binding of a key for a value that the value comparer finds equal, its key included + /// (ADR 0001, D14). + /// + [] + member build : action : (SortedMap<'Key, 'Value>.Builder -> unit) -> SortedMap<'Key, 'Value> + + /// + /// Creates an empty builder with the comparers. + /// + /// The builder. + /// + /// O(1). The member wraps + /// . + /// + [] + member builder : unit -> SortedMap<'Key, 'Value>.Builder + + // ---- Properties ---- + + /// + /// The comparer that orders the keys. + /// + /// + /// O(1). The default value of the struct holds no comparer and returns + /// , as + /// does for a missing key comparer (ADR 0001, + /// D14); it orders strings by the rules of the current culture, not ordinally as F# generic comparison does. + /// + member KeyComparer : IComparer<'Key> + + /// + /// The equality comparer of the values. + /// + /// + /// O(1). The default value of the struct holds no comparer and returns + /// . + /// + member ValueComparer : IEqualityComparer<'Value> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fs new file mode 100644 index 00000000..14cde8b2 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fs @@ -0,0 +1,53 @@ +[] +module FSharp.Collections.Immutable.SortedMapExtensions + +open System.Collections.Generic +open System.Collections.Immutable + +// The members are the members of CustomComparersSortedMap with the default comparers, so that every shape of the bindings +// has one implementation (ADR 0001, D13). Each member declares the type parameters of the map that it creates, because +// only its own type parameters can require F# comparison of the keys (FS0340). + +/// The comparer struct with the default comparers: F# generic comparison of the keys, one cached comparer per type, and the +/// default equality comparer of the values (D5). +let private defaultComparers<'K, 'V when 'K : not null and 'K : comparison> () = + CustomComparersSortedMap<'K, 'V> DefaultComparer<'K>.Instance + +// The extended type repeats the constraint of the BCL type (FS0957) +type ImmutableSortedDictionary<'Key, 'Value when 'Key : not null> with + + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> (elements : KeyValuePair<'K, 'V> array) = + (defaultComparers<'K, 'V>()).ofArray elements + + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> (elements : ('K * 'V) array) = + (defaultComparers<'K, 'V>()).ofArray elements + + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> (elements : struct ('K * 'V) array) = + (defaultComparers<'K, 'V>()).ofArray elements + + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> (elements : KeyValuePair<'K, 'V> list) = + (defaultComparers<'K, 'V>()).ofList elements + + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> (elements : ('K * 'V) list) = + (defaultComparers<'K, 'V>()).ofList elements + + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> (elements : struct ('K * 'V) list) = + (defaultComparers<'K, 'V>()).ofList elements + + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> (elements : seq>) = + (defaultComparers<'K, 'V>()).ofSeq elements + + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> (elements : seq<'K * 'V>) = + (defaultComparers<'K, 'V>()).ofSeq elements + + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> (elements : seq) = + (defaultComparers<'K, 'V>()).ofSeq elements diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fsi new file mode 100644 index 00000000..ffddb1ad --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.Extensions.fsi @@ -0,0 +1,173 @@ +/// +/// The creation functions of that take bindings in every shape: +/// static members of , each overloaded for +/// key/value pairs, reference tuples and struct tuples (ADR 0001, D13). +/// +/// +/// +/// Through the abbreviation , SortedMap.ofSeq pairs reaches +/// these members, because the module has no functions with their +/// names, and overload resolution picks the shape of the bindings; pairs |> SortedMap.ofSeq works too. Inside +/// generic code, the bindings need a type annotation (FS0041). +/// +/// +/// The members create sorted maps that order their keys with F# generic comparison, +/// , and compare their values with +/// : they are the members of +/// with those comparers. A later binding of a key +/// replaces an earlier one, as in +/// , +/// unless the default comparer of the values finds the two values equal: then the earlier binding stays, its key included, +/// as in the builder of (ADR 0001, D14). +/// +/// +/// F# generic comparison needs keys with F# comparison, and a member cannot constrain the type parameters of the type that +/// it extends (FS0340). Each member therefore declares type parameters of its own, 'K and 'V, for the keys and +/// the values of the sorted map that it creates. +/// +/// +/// A repeated key never throws, where +/// +/// and ToImmutableSortedDictionary reject another value of it. To reject it, add the bindings to an empty map: +/// SortedMap.empty |> SortedMap.addRange pairs, or +/// (SortedMap.forComparers keyComparer valueComparer).empty |> SortedMap.addRange pairs for other comparers; it throws +/// like the BCL. +/// +/// +/// Static extension members are an F# feature: C# does not see them as members of +/// , and a reflection over the module +/// lists none of ofArray, ofList and ofSeq. +/// They compile to public static methods of this module, SortedMapExtensions.OfArray, OfList and OfSeq, +/// with one overload per shape. C# can call those, or the members of +/// , or the BCL. +/// +/// +[] +module FSharp.Collections.Immutable.SortedMapExtensions + +open System.Collections.Generic +open System.Collections.Immutable + +// The extended type repeats the constraint of the BCL type (FS0957) +type ImmutableSortedDictionary<'Key, 'Value when 'Key : not null> with + + /// + /// Creates a sorted map from an array of key/value pairs. + /// + /// The input array of key/value pairs. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> : + elements : KeyValuePair<'K, 'V> array -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from an array of reference tuples. + /// + /// The input array of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> : elements : ('K * 'V) array -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from an array of struct tuples. + /// + /// The input array of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// Thrown when is . + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofArray<'K, 'V when 'K : not null and 'K : comparison> : elements : struct ('K * 'V) array -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a list of key/value pairs. + /// + /// The input list of key/value pairs. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> : + elements : KeyValuePair<'K, 'V> list -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a list of reference tuples. + /// + /// The input list of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> : elements : ('K * 'V) list -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a list of struct tuples. + /// + /// The input list of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofList<'K, 'V when 'K : not null and 'K : comparison> : elements : struct ('K * 'V) list -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a sequence of key/value pairs, such as a dictionary or another map. + /// + /// The input sequence of key/value pairs. + /// + /// The sorted map of the bindings, which orders its keys with F# generic comparison. A sorted map with the default + /// comparers comes back itself. + /// + /// Thrown when is or a default flat list. + /// + /// O(n log n); O(1) for a sorted map that orders its keys with F# generic comparison. A later binding of a key replaces + /// an earlier one, unless the default comparer of the values finds the two values equal: then the earlier binding + /// stays, its key included (ADR 0001, D14). Keys that only F# generic comparison considers equal follow the same rule + /// in the order of enumeration. + /// + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> : elements : seq> -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a sequence of reference tuples. + /// + /// The input sequence of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> : elements : seq<'K * 'V> -> SortedMap<'K, 'V> + + /// + /// Creates a sorted map from a sequence of struct tuples. + /// + /// The input sequence of key/value tuples. + /// The sorted map of the bindings, which orders its keys with F# generic comparison. + /// Thrown when is or a default flat list. + /// + /// O(n log n). A later binding of a key replaces an earlier one, unless the default comparer of the values finds the + /// two values equal: then the earlier binding stays, its key included (ADR 0001, D14). + /// + [] + static member ofSeq<'K, 'V when 'K : not null and 'K : comparison> : elements : seq -> SortedMap<'K, 'V> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fs new file mode 100644 index 00000000..3c9a995a --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fs @@ -0,0 +1,402 @@ +[] +module FSharp.Collections.Immutable.SortedMap + +open System.Collections.Generic +open System.Collections.Immutable +open System.Linq + +// The functions call the members of ImmutableSortedDictionary where it has them (ADR 0001, D5) and visit the bindings with +// the algorithms that the map modules share (`Dictionaries` in Helper.fs). The transformations fill a builder that has the +// comparers of the input, so that they keep them (D5, comparers). The functions that create a map from bindings are the +// members of CustomComparersSortedMap (D12) and the static members of SortedMapExtensions (D13); the module has none of +// their names, so that `SortedMap.ofSeq` reaches the overloads of the extensions. + +/// The struct with the default comparers: F# generic comparison of the keys, one cached comparer per type (D5), and the +/// default equality comparer of the values. The creation functions are its members, so that each has one implementation +/// (D12). +let private withDefaultComparers<'Key, 'Value when 'Key : not null and 'Key : comparison> () = + CustomComparersSortedMap<'Key, 'Value> DefaultComparer<'Key>.Instance + +/// The empty sorted map with the default comparers, created once per type of the keys and the values, as the empty F# map +/// is. `ImmutableSortedDictionary<'Key, 'Value>.Empty` orders its keys with `Comparer<'Key>.Default`, which is +/// culture-sensitive for strings, so `empty` is this map instead. +type private DefaultSortedMap<'Key, 'Value when 'Key : not null and 'Key : comparison> private () = + + static let empty = (withDefaultComparers<'Key, 'Value>()).empty + + static member Empty : SortedMap<'Key, 'Value> = empty + +// ---- Creation ---- + +[] +[] +let empty<'Key, 'Value when 'Key : not null and 'Key : comparison> : SortedMap<'Key, 'Value> = + DefaultSortedMap<'Key, 'Value>.Empty + +[] +let forComparers (keyComparer : IComparer<'Key>) (valueComparer : IEqualityComparer<'Value>) = + CustomComparersSortedMap<'Key, 'Value>(keyComparer, valueComparer) + +[] +let forKeyComparer (keyComparer : IComparer<'Key>) = CustomComparersSortedMap<'Key, 'Value> keyComparer + +[] +let ofSeqBy (projection : 'Value -> 'Key) (source : seq<'Value>) : SortedMap<'Key, 'Value> = + (withDefaultComparers ()).ofSeqBy projection source + +[] +let singleton (key : 'Key) (value : 'Value) : SortedMap<'Key, 'Value> = (withDefaultComparers ()).singleton key value + +// ---- Builder ---- + +[] +let build (action : SortedMap<'Key, 'Value>.Builder -> unit) : SortedMap<'Key, 'Value> = + (withDefaultComparers<'Key, 'Value>()).build action + +[] +let builder () : SortedMap<'Key, 'Value>.Builder = (withDefaultComparers<'Key, 'Value>()).builder() + +[] +let ofBuilder (builder : SortedMap<'Key, 'Value>.Builder) = + checkNotNull (nameof builder) builder + builder.ToImmutable () + +[] +let toBuilder (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ToBuilder () + +[] +let update (action : SortedMap<'Key, 'Value>.Builder -> unit) (table : SortedMap<'Key, 'Value>) : SortedMap<'Key, 'Value> = + checkNotNull (nameof table) table + // The builder starts with the bindings and the comparers of the map, and ToImmutable returns the map itself when the + // action changed nothing + let builder = table.ToBuilder () + action builder + builder.ToImmutable () + +// ---- Properties ---- + +[] +let count (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Count + +[] +let isEmpty (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.IsEmpty + +[] +let keyComparer (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.KeyComparer + +[] +let keys (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The explicit implementation of IDictionary is a view whose count and lookup ask the map, as Map.keys is + (table :> IDictionary<'Key, 'Value>).Keys + +[] +let valueComparer (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ValueComparer + +[] +let values (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + (table :> IDictionary<'Key, 'Value>).Values + +// ---- Access ---- + +[] +let find (key : 'Key) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> value + | false, _ -> Raise.keyNotFound () + +[] +let maxKeyValue (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + if table.IsEmpty then + // Map.maxKeyValue throws the exception with its default message + Raise.keyNotFound () + else + // The dictionary enumerates only forwards and hides its tree, so the largest key takes a pass over the bindings + let pair = Enumerable.Last table + struct (pair.Key, pair.Value) + +[] +let minKeyValue (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + if table.IsEmpty then + // Map.minKeyValue throws the exception with its default message + Raise.keyNotFound () + else + // The enumerator descends the left spine of the tree to the smallest key, which takes O(log n) + let pair = Enumerable.First table + struct (pair.Key, pair.Value) + +[] +let tryFind (key : 'Key) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + +[] +let tryGetKey (key : 'Key) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match table.TryGetKey key with + | true, actualKey -> ValueSome actualKey + | false, _ -> ValueNone + +// ---- Search ---- + +[] +let containsBinding (key : 'Key) (value : 'Value) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The value comparer of the map decides whether the stored value equals the value, which no other function can ask + table.Contains (KeyValuePair (key, value)) + +[] +let containsKey (key : 'Key) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsKey key + +[] +let containsValue (value : 'Value) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.ContainsValue value + +// exists and forall loop over the struct enumerator of the sorted map, which allocates nothing. The algorithm that the map +// modules share, over the key/value pairs of any dictionary, allocated 56 bytes per call, the boxed enumerator; LINQ Any +// and All allocated 136 bytes, and Seq.exists and Seq.forall 80 bytes (measured in Release; a measured faster path +// replaces a higher priority, D5). Dispose returns the pooled stack of the enumerator. + +[] +let exists (predicate : 'Key -> 'Value -> bool) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let mutable enumerator = table.GetEnumerator () + + try + let mutable found = false + + while not found && enumerator.MoveNext () do + let pair = enumerator.Current + found <- predicate pair.Key pair.Value + + found + finally + enumerator.Dispose () + +[] +let findKey predicate (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + // Map.findKey searches with Seq.pick, whose message differs from the one of Map.find + match Dictionaries.tryFindKey predicate table with + | ValueSome key -> key + | ValueNone -> Raise.indexNotFound () + +[] +let forall (predicate : 'Key -> 'Value -> bool) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let mutable enumerator = table.GetEnumerator () + + try + let mutable satisfied = true + + while satisfied && enumerator.MoveNext () do + let pair = enumerator.Current + satisfied <- predicate pair.Key pair.Value + + satisfied + finally + enumerator.Dispose () + +[] +let pick (chooser : 'Key -> 'Value -> 'U voption) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + match Dictionaries.tryPick chooser table with + | ValueSome value -> value + | ValueNone -> Raise.keyNotFound () + +[] +let tryFindKey predicate (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryFindKey predicate table + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U voption) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.tryPick chooser table + +// ---- Transformation ---- + +// add is ImmutableSortedDictionary.Add: it throws for a key bound to another value and returns the map itself for an equal +// binding. Map.add replaces the value, which setItem does (ADR 0001, D14) +[] +let add (key : 'Key) (value : 'Value) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Add (key, value) + +[] +let addRange (pairs : seq>) (table : SortedMap<'Key, 'Value>) = + // A default flat list given as a sequence is as good as a null one: it fails when the BCL member enumerates it, and + // the exception would name the parameter of the member (ADR 0001, D6) + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange pairs + +[] +let change (key : 'Key) (f : 'Value voption -> 'Value voption) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + + let current = + match table.TryGetValue key with + | true, value -> ValueSome value + | false, _ -> ValueNone + + match f current with + | ValueSome value -> table.SetItem (key, value) + | ValueNone -> table.Remove key + +[] +let choose (chooser : 'Key -> 'Value -> 'U voption) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // The keys keep their type, their comparer and so their order; the new values get the default comparer of their type + let builder = ImmutableSortedDictionary.CreateBuilder<'Key, 'U> table.KeyComparer + Dictionaries.choose chooser builder table + builder.ToImmutable () + +[] +let clear (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Clear () + +[] +let filter predicate (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + // A builder over the input shares the bindings that stay and returns the input itself when every binding stays + let builder = table.ToBuilder () + Dictionaries.filter predicate builder table + builder.ToImmutable () + +[] +let map (mapping : 'Key -> 'Value -> 'U) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let builder = ImmutableSortedDictionary.CreateBuilder<'Key, 'U> table.KeyComparer + Dictionaries.map mapping builder table + builder.ToImmutable () + +[] +let remove (key : 'Key) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.Remove key + +[] +let removeRange (keys : seq<'Key>) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof keys) keys + checkNotNull (nameof table) table + table.RemoveRange keys + +[] +let setItem (key : 'Key) (value : 'Value) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + table.SetItem (key, value) + +[] +let setItems (pairs : seq>) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems pairs + +// ---- Folding ---- + +[] +let fold (folder : 'State -> 'Key -> 'Value -> 'State) (state : 'State) (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.fold folder state table + +[] +let foldBack (folder : 'Key -> 'Value -> 'State -> 'State) (table : SortedMap<'Key, 'Value>) (state : 'State) = + checkNotNull (nameof table) table + // The bindings come in ascending order, so the reverse order of enumeration is the descending order of Map.foldBack + Dictionaries.foldBack folder table state + +[] +let iter action (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.iter action table + +// ---- Partitioning ---- + +[] +let partition predicate (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + let accepted = table.ToBuilder () + let rejected = table.Clear().ToBuilder() + Dictionaries.partition predicate accepted rejected table + struct (accepted.ToImmutable (), rejected.ToImmutable ()) + +// ---- Conversion ---- + +[] +let toArray (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray structPair table + +[] +let toList (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList structPair table + +[] +let toSeq (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq structPair table + +// The submodules take the bindings as tuples, convert them to key/value pairs on demand and call the members that the +// functions of the module call, so that every shape has the semantics of the BCL member (D13). +[] +module Tuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq<'Key * 'Value>) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + + [] + let setItems (pairs : seq<'Key * 'Value>) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs Dictionaries.splitTuple pairs) + +[] +module ValueTuple = + + // ---- Transformation ---- + + [] + let addRange (pairs : seq) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.AddRange (Dictionaries.toKeyValuePairs id pairs) + + [] + let setItems (pairs : seq) (table : SortedMap<'Key, 'Value>) = + checkNotNullOrDefaultSeq (nameof pairs) pairs + checkNotNull (nameof table) table + table.SetItems (Dictionaries.toKeyValuePairs id pairs) diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fsi new file mode 100644 index 00000000..ff9caf67 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMap.fsi @@ -0,0 +1,967 @@ +/// +/// Functions for , the immutable sorted map. +/// +/// +/// +/// The module mirrors : each function with a name of that module has +/// its semantics, its argument checks and its exceptions (ADR 0001), except +/// . +/// It is the of the BCL: it throws for +/// a key that is bound to another value, where +/// +/// replaces the value, and +/// +/// replaces it (ADR 0001, D14). The other functions wrap a member of +/// and keep its semantics and its exceptions. +/// +/// +/// A sorted map enumerates its bindings in the ascending order of the keys in its key comparer. The functions that visit +/// the bindings one by one follow that order, and the functions that visit them from the end follow the descending order, +/// as the functions of do. +/// +/// +/// A sorted map orders its keys with the comparer that it was created with, and compares its values with an equality +/// comparer. The functions of the module create sorted maps that order their keys with F# generic comparison, +/// , as +/// does, and not with +/// : a sorted map of strings orders them ordinally, not by the +/// rules of a culture. The values compare with . +/// +/// and +/// return a , which creates sorted maps with other +/// comparers (ADR 0001, D12). Every transformation keeps the key comparer. The transformations that keep the type of the +/// values keep the value comparer, and the transformations that change it give the new values the default comparer of +/// their type. +/// +/// +/// A new value of a key that the value comparer finds equal to the current one leaves the binding as it is, its key +/// included, in the functions that set bindings and in the creation functions, as in +/// (ADR 0001, D14). +/// +/// +/// The single-key idioms of the BCL are functions of the module or compositions of them. +/// is +/// , +/// and +/// is SortedMap.tryFind key table |> ValueOption.defaultValue defaultValue. Adding a binding only when the key has +/// none is +/// +/// with a function that keeps an existing value. has no +/// member for a sorted dictionary, but its +/// replaces a field +/// atomically with the result of a function of the sorted map that it holds, such as a function of this module, and takes a +/// reference to the field. +/// +/// +/// A builder is the BCL builder: the module creates it (builder, toBuilder, build and update) +/// and turns it into a sorted map (ofBuilder), and the members of the builder, such as Add, AddRange, +/// Remove, RemoveRange and the indexer, are called on it. There is no builder module for maps (ADR 0001, D2). +/// +/// +/// A binding comes as a key/value pair, a reference tuple or a struct tuple (ADR 0001, D13). SortedMap.ofArray, +/// SortedMap.ofList and SortedMap.ofSeq take every shape: they are the static members of +/// , which the module leaves visible by having no functions +/// with their names. +/// and +/// take key/value pairs, and the nested modules and +/// take reference and struct tuples. +/// +/// +/// A map, sequence, builder or comparer throws , and so +/// does a default flat list that is given as a sequence: it fails when a function enumerates it, so it is as good as a +/// sequence (ADR 0001, D6). +/// +/// +/// 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.SortedMap + +open System.Collections.Generic +open System.Collections.Immutable + +// ---- Creation ---- + +/// +/// The empty sorted map, which orders its keys with F# generic comparison. +/// +/// +/// O(1). The value is with the key comparer +/// , created once per type of the keys and +/// the values, as the empty is. +/// +[] +[] +val empty<'Key, 'Value when 'Key : not null and 'Key : comparison> : SortedMap<'Key, 'Value> + +/// +/// Holds a comparer that orders the keys and an equality comparer of the values, to create sorted maps with them. +/// +/// The comparer that orders the keys. +/// The equality comparer of the values. +/// The struct whose members create sorted maps with the comparers. +/// +/// Thrown when or is . +/// +/// +/// +/// O(1); the struct allocates nothing. Its members have the names of the creation functions of the module (ADR 0001, D12). +/// +/// +/// The ofSeq member of the struct also gives an existing sorted map these comparers, which is how to change the +/// comparers of a map. Unlike +/// it rebuilds the map unless the key comparer is the same instance, and it merges keys that become equal instead of +/// throwing; the remarks of have the details. +/// +/// +/// +/// +/// let comparers = SortedMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.Ordinal +/// let table = comparers.ofSeq [ "two", "dos"; "One", "uno" ] +/// +/// Evaluates to a sorted map of "One" and "two", in this order, whose keys ignore the case and whose values compare +/// ordinally. +/// +/// +/// +/// let ignoreCase = (SortedMap.forComparers StringComparer.OrdinalIgnoreCase (SortedMap.valueComparer table)).ofSeq table +/// +/// Evaluates to a sorted map with the bindings of table whose keys ignore the case, in the order of that comparer, +/// and whose values keep the value comparer of table. +/// +[] +val forComparers : + keyComparer : IComparer<'Key> -> valueComparer : IEqualityComparer<'Value> -> CustomComparersSortedMap<'Key, 'Value> + +/// +/// Holds a comparer that orders the keys, to create sorted maps with it. +/// +/// The comparer that orders the keys. +/// +/// The struct whose members create sorted maps with the key comparer, and with the default comparer of the values. +/// +/// Thrown when is . +/// +/// +/// O(1); the struct allocates nothing. Its members have the names of the creation functions of the module (ADR 0001, D12). +/// +/// +/// The ofSeq member of the struct also gives an existing sorted map this key comparer, with the default comparer of +/// the values: the value comparer of the map is replaced, where +/// +/// keeps it. Use +/// +/// with the value comparer of the map to keep it. +/// +/// +/// +/// +/// let table = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [ "two", 2; "one", 1; "ONE", 3 ] +/// +/// Evaluates to a sorted map whose keys ignore the case, with the bindings of "ONE" to 3 and "two" to 2, in this order. +/// +[] +val forKeyComparer : keyComparer : IComparer<'Key> -> CustomComparersSortedMap<'Key, 'Value> + +/// +/// Creates a sorted map of the elements of a sequence under the keys that a projection returns for them. +/// +/// The function that returns the key of an element. +/// The input sequence, whose elements become the values. +/// The sorted map of the elements under their keys, which orders the keys with F# generic comparison. +/// Thrown when is or a default flat list. +/// +/// Thrown when two elements have the same key and the default comparer of the values finds them different. +/// +/// +/// O(n log n). The function wraps +/// +/// with the element itself as the value and , +/// which rejects a repeated key with another element and accepts one with an element that the default comparer of the +/// values finds equal: the first element stays, its key included (ADR 0001, D14). The value of a binding is the element +/// itself; with an element selector, as ToImmutableSortedDictionary takes one, +/// SortedMap.empty |> SortedMap.addRange (source |> Seq.map (fun x -> KeyValuePair (key x, element x))) rejects a +/// repeated key like the BCL does, and SortedMap.ofSeq of the mapped pairs lets the last binding win. +/// +[] +val ofSeqBy : + projection : ('Value -> 'Key) -> source : seq<'Value> -> SortedMap<'Key, 'Value> when 'Key : not null and 'Key : comparison + +/// Creates a sorted map of one binding. +/// The key. +/// The value. +/// +/// The sorted map of the binding, which orders its keys with F# generic comparison and compares its values with the +/// default comparer. +/// +/// O(1). +[] +val singleton : key : 'Key -> value : 'Value -> SortedMap<'Key, 'Value> when 'Key : not null and 'Key : comparison + +// ---- Builder ---- + +/// +/// Creates a sorted map with a builder that an action fills. +/// +/// +/// The action that sets the bindings in the builder, which orders the keys with F# generic comparison and compares the +/// values with the default comparer. +/// +/// +/// The sorted map of the bindings that the action set, with F# generic comparison of the keys and the default comparer of +/// the values. +/// +/// +/// +/// O(1) apart from the cost of the action, which sets each binding of the builder in O(log n), and from freezing the nodes +/// that the action changed: the sorted map shares the nodes of the builder instead of copying the entries. The function +/// creates the builder with +/// +/// and returns it with , as +/// +/// does for other comparers. +/// +/// +/// The indexer of the builder keeps the earlier binding of a key for a value that the value comparer finds equal, its key +/// included (ADR 0001, D14). +/// +/// +/// +/// +/// let table = SortedMap.build (fun builder -> for i in 3 .. -1 .. 1 do builder[i] <- i * i) +/// +/// Evaluates to a sorted map of 1 to 1, 2 to 4 and 3 to 9, in this order. +/// +[] +val build : + action : (SortedMap<'Key, 'Value>.Builder -> unit) -> SortedMap<'Key, 'Value> when 'Key : not null and 'Key : comparison + +/// +/// Creates an empty builder that orders the keys with F# generic comparison. +/// +/// The builder, whose values compare with the default comparer. +/// +/// O(1). The function wraps +/// +/// with . +/// +[] +val builder : unit -> SortedMap<'Key, 'Value>.Builder when 'Key : not null and 'Key : comparison + +/// +/// Returns the contents of a builder as a sorted map. +/// +/// The builder, which stays usable. +/// The sorted map of the bindings of the builder, with its comparers. +/// Thrown when is . +/// +/// O(1) apart from freezing the nodes that changed since the builder last returned a sorted map: the sorted map shares the +/// nodes of the builder instead of copying the entries. A builder that has not changed since returns the same sorted map +/// again, and a builder that was just filled freezes all its nodes, which takes O(n). The function wraps +/// . +/// +[] +val ofBuilder : builder : SortedMap<'Key, 'Value>.Builder -> SortedMap<'Key, 'Value> + +/// +/// Creates a builder that contains the bindings of a sorted map. +/// +/// The input sorted map, which stays unchanged. +/// The builder, with the comparers of the sorted map. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val toBuilder : table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value>.Builder + +/// +/// Creates a sorted map from a builder over the bindings of a sorted map, after an action changes the builder. +/// +/// The action that changes the builder, which has the comparers of the sorted map. +/// The input sorted map, which stays unchanged. +/// +/// The sorted map of the changed builder, with the comparers of the input; the input itself when the action changed +/// nothing. +/// +/// Thrown when is . +/// +/// O(1) apart from the cost of the action, which changes each binding of the builder in O(log n), and from freezing the +/// nodes that the action changed: the sorted map shares the nodes of the builder, and so the nodes of the input, instead of +/// copying the entries. The function creates the builder with +/// and returns it with +/// . +/// +/// +/// +/// let squares = SortedMap.build (fun builder -> for i in 1..3 do builder[i] <- i * i) +/// let table = squares |> SortedMap.update (fun builder -> builder.Remove 1 |> ignore; builder[4] <- 16) +/// +/// Evaluates to a sorted map of 2 to 4, 3 to 9 and 4 to 16, in this order, and squares stays unchanged. +/// +[] +val update : action : (SortedMap<'Key, 'Value>.Builder -> unit) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +// ---- Properties ---- + +/// Returns the number of bindings of the sorted map. +/// The input sorted map. +/// The number of bindings. +/// Thrown when is . +/// O(1). +[] +val count : table : SortedMap<'Key, 'Value> -> int + +/// Returns whether the sorted map has no bindings. +/// The input sorted map. +/// when the sorted map is empty. +/// Thrown when is . +/// O(1). +[] +val isEmpty : table : SortedMap<'Key, 'Value> -> bool + +/// +/// Returns the comparer that orders the keys. +/// +/// The input sorted map. +/// The key comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val keyComparer : table : SortedMap<'Key, 'Value> -> IComparer<'Key> + +/// Returns the keys of the sorted map. +/// The input sorted map. +/// +/// A read-only view of the keys in ascending order, whose count takes O(1) and whose lookup takes O(log n) in the key +/// comparer. +/// +/// Thrown when is . +/// O(1). +[] +val keys : table : SortedMap<'Key, 'Value> -> ICollection<'Key> + +/// +/// Returns the equality comparer of the values. +/// +/// The input sorted map. +/// The value comparer. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val valueComparer : table : SortedMap<'Key, 'Value> -> IEqualityComparer<'Value> + +/// +/// Returns the values of the sorted map, including the duplicates. +/// +/// The input sorted map. +/// +/// A read-only view of the values in the ascending order of their keys, whose count takes O(1) and whose lookup takes +/// O(n) in the value comparer. +/// +/// Thrown when is . +/// +/// O(1). +/// +[] +val values : table : SortedMap<'Key, 'Value> -> ICollection<'Value> + +// ---- Access ---- + +/// Returns the value of a key. +/// The key to find. +/// The input sorted map. +/// The value of the key. +/// Thrown when is . +/// Thrown when the sorted map has no binding of the key. +/// O(log n). +[] +val find : key : 'Key -> table : SortedMap<'Key, 'Value> -> 'Value + +/// +/// Returns the binding of the largest key of the sorted map. +/// +/// The input sorted map. +/// The key and the value of the last binding in the order of the keys. +/// Thrown when is . +/// Thrown when the sorted map is empty. +/// +/// O(n), where +/// +/// takes O(log n): enumerates its bindings only +/// in ascending order and gives no other access to its largest key, so the function visits every binding. +/// +[] +val maxKeyValue : table : SortedMap<'Key, 'Value> -> struct ('Key * 'Value) + +/// Returns the binding of the smallest key of the sorted map. +/// The input sorted map. +/// The key and the value of the first binding in the order of the keys. +/// Thrown when is . +/// Thrown when the sorted map is empty. +/// O(log n). +[] +val minKeyValue : table : SortedMap<'Key, 'Value> -> struct ('Key * 'Value) + +/// +/// Returns the value of a key, or ValueNone when the sorted map has no binding of the key. +/// +/// The key to find. +/// The input sorted map. +/// The value of the key, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function is the TryGetValue of the BCL with a voption instead of the out parameter. +/// GetValueOrDefault is SortedMap.tryFind key table |> ValueOption.defaultValue defaultValue: one lookup, +/// and a voption is a struct. +/// +[] +val tryFind : key : 'Key -> table : SortedMap<'Key, 'Value> -> 'Value voption + +/// +/// Returns the key of the sorted map that equals a key in the key comparer, or ValueNone. +/// +/// The key to find. +/// The input sorted map. +/// The key that the sorted map holds, which can differ from the given one, or ValueNone. +/// Thrown when is . +/// +/// O(log n). The function wraps . +/// +[] +val tryGetKey : key : 'Key -> table : SortedMap<'Key, 'Value> -> 'Key voption + +// ---- Search ---- + +/// +/// Returns whether the sorted map binds a key to a value, comparing the values with the value comparer. +/// +/// The key to find. +/// The value that the key must be bound to. +/// The input sorted map. +/// +/// when the sorted map has a binding of the key whose value the value comparer finds equal to +/// . +/// +/// +/// Thrown when or is . +/// +/// +/// +/// O(log n). The function wraps +/// . +/// The key comparer of the sorted map finds the key, and a key without a binding gives . A +/// key throws, as it does in +/// ; +/// the parameter of the exception is the one that the member names, Key. +/// +/// +/// The value comparer of the sorted map decides whether the values are equal. No composition of the other functions asks +/// it: +/// followed by = compares with F# equality, which for a comparer that ignores the case finds "Hello" and +/// "HELLO" different. +/// +/// +/// +/// +/// let table = (SortedMap.forComparers StringComparer.Ordinal StringComparer.OrdinalIgnoreCase).ofSeq [ "one", "Uno"; "two", "Dos" ] +/// let found = table |> SortedMap.containsBinding "one" "UNO" +/// +/// Evaluates to true, where SortedMap.tryFind "one" table is ValueSome "Uno". +/// +[] +val containsBinding : key : 'Key -> value : 'Value -> table : SortedMap<'Key, 'Value> -> bool + +/// Returns whether the sorted map has a binding of a key. +/// The key to find. +/// The input sorted map. +/// when the sorted map has a binding of the key. +/// Thrown when is . +/// O(log n). +[] +val containsKey : key : 'Key -> table : SortedMap<'Key, 'Value> -> bool + +/// +/// Returns whether the sorted map has a binding of a value, in the value comparer. +/// +/// The value to find. +/// The input sorted map. +/// when a value of the sorted map equals the given one. +/// Thrown when is . +/// +/// O(n). The function wraps . +/// +[] +val containsValue : value : 'Value -> table : SortedMap<'Key, 'Value> -> bool + +/// Returns whether any binding satisfies the predicate. +/// The function to test the bindings. +/// The input sorted map. +/// when a binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val exists : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> bool + +/// +/// Returns the key of the first binding, in the order of the keys, that satisfies the predicate. +/// +/// The function to test the bindings. +/// The input sorted map. +/// The key of the first binding that satisfies the predicate. +/// Thrown when is . +/// Thrown when no binding satisfies the predicate. +/// +/// O(n). +/// +[] +val findKey : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> 'Key + +/// Returns whether all bindings satisfy the predicate. +/// The function to test the bindings. +/// The input sorted map. +/// when every binding satisfies the predicate. +/// Thrown when is . +/// O(n). +[] +val forall : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> bool + +/// +/// Returns the first value, in the order of the keys, that the chooser returns for the bindings. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input sorted map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns ValueNone for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U voption) -> table : SortedMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of the keys, that satisfies the predicate, or ValueNone. +/// +/// The function to test the bindings. +/// The input sorted map. +/// The key of the first binding that satisfies the predicate, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> 'Key voption + +/// +/// Returns the first value, in the order of the keys, that the chooser returns for the bindings, or ValueNone. +/// +/// The function that returns a value, or ValueNone to go on. +/// The input sorted map. +/// The first chosen value, or ValueNone. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U voption) -> table : SortedMap<'Key, 'Value> -> 'U voption + +// ---- Transformation ---- + +/// +/// Returns a sorted map with a binding of a key to a value, for a key that is not bound to another value. +/// +/// The key. +/// The value. +/// The input sorted map. +/// The sorted map with the binding; the input itself when it already has the binding. +/// Thrown when is . +/// +/// Thrown when the sorted map binds the key to a value that the value comparer finds different. +/// +/// +/// +/// O(log n). The function wraps and +/// behaves as it does (ADR 0001, D14): it does not replace the value of a key, where +/// +/// does. When the value comparer of the sorted map finds the value equal to the current value of the key, the binding stays +/// as it is, its key included, and the input itself comes back. +/// +/// +/// Use +/// to replace the value of a key, and +/// +/// to add a binding only when the key has none, or to compute the new value from the current one. +/// +/// +[] +val add : key : 'Key -> value : 'Value -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map with the bindings of key/value pairs added. +/// +/// The key/value pairs to add. +/// The input sorted map. +/// The sorted map with the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// Thrown when a pair has the key of a binding of the sorted map or of another pair, and a value that the value comparer +/// finds different. +/// +/// +/// O(m log(n + m)). The function wraps +/// , +/// which rejects another value of a key and accepts a pair whose value the value comparer finds equal to the value of the +/// key, leaving that binding as it is, its key included (ADR 0001, D14). Use +/// +/// to replace the values, and and +/// to add tuples. +/// +[] +val addRange : pairs : seq> -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or ValueNone, and returns the new value or ValueNone to remove +/// the binding. +/// +/// The input sorted map. +/// The sorted map with the changed binding; the input itself when nothing changes. +/// Thrown when is . +/// +/// +/// O(log n). When returns a value that the value comparer of the sorted map finds equal to the +/// current value, the binding stays as it is, its key included, as in +/// (ADR 0001, D14). +/// +/// +/// Adding a binding only when the key has none is +/// SortedMap.change key (function ValueNone -> ValueSome value | existing -> existing). +/// +/// +[] +val change : key : 'Key -> f : ('Value voption -> 'Value voption) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or ValueNone to drop the binding. +/// The input sorted map. +/// +/// The sorted map of the chosen values, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U voption) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'U> + +/// +/// Returns an empty sorted map with the comparers of a sorted map. +/// +/// The input sorted map. +/// The empty sorted map; the input itself when it is empty. +/// Thrown when is . +/// +/// O(1). The function wraps . +/// +[] +val clear : table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// Returns the bindings that satisfy the predicate. +/// The function to test the bindings. +/// The input sorted map. +/// +/// The sorted map of the bindings that satisfy the predicate, with the comparers of the input; the input itself when +/// every binding does. +/// +/// Thrown when is . +/// O(n log n). +[] +val filter : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns the results of a function applied to each binding, under the keys of the bindings. +/// +/// The function to transform the bindings. +/// The input sorted map. +/// +/// The sorted map of the results, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val map : mapping : ('Key -> 'Value -> 'U) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'U> + +/// Returns a sorted map without the binding of a key. +/// The key to remove. +/// The input sorted map. +/// The sorted map without the binding; the input itself when it has no binding of the key. +/// Thrown when is . +/// O(log n). +[] +val remove : key : 'Key -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map without the bindings of keys. +/// +/// The keys to remove; a key without a binding is skipped. +/// The input sorted map. +/// The sorted map without the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log n). The function wraps +/// . +/// +[] +val removeRange : keys : seq<'Key> -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map with a binding of a key to a value, which replaces an existing binding of the key. +/// +/// The key. +/// The value. +/// The input sorted map. +/// The sorted map with the binding; the input itself when the binding stays as it is. +/// Thrown when is . +/// +/// O(log n). The function wraps and +/// replaces the value of a key, as +/// +/// does, where +/// +/// throws for a key that is bound to another value. When the value comparer of the sorted map finds the value equal to the +/// current value of the key, the binding stays as it is, its key included (ADR 0001, D14). +/// +[] +val setItem : key : 'Key -> value : 'Value -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map with the bindings of key/value pairs, which replace the existing bindings of their keys. +/// +/// The key/value pairs to set; a later pair of a key replaces an earlier one. +/// The input sorted map. +/// The sorted map with the bindings. +/// Thrown when is or a default flat list, or is . +/// +/// O(m log(n + m)). The function wraps +/// . +/// A pair whose value the value comparer finds equal to the current value of its key, or to the value of an earlier pair +/// of the key, leaves that binding as it is, its key included (ADR 0001, D14). +/// and +/// set tuples. +/// +[] +val setItems : pairs : seq> -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +// ---- Folding ---- + +/// +/// Applies a function to a state and each binding, in the ascending order of the keys. +/// +/// The function that computes the next state. +/// The initial state. +/// The input sorted map. +/// The final state. +/// Thrown when is . +/// +/// O(n). +/// +[] +val fold : folder : ('State -> 'Key -> 'Value -> 'State) -> state : 'State -> table : SortedMap<'Key, 'Value> -> 'State + +/// +/// Applies a function to each binding and a state, in the descending order of the keys. +/// +/// The function that computes the next state. +/// The input sorted map. +/// The initial state. +/// The final state. +/// Thrown when is . +/// +/// O(n). The function visits the bindings from the largest key to the smallest one, as +/// +/// does. It copies the bindings first, because the sorted map enumerates them only in ascending order. +/// +[] +val foldBack : folder : ('Key -> 'Value -> 'State -> 'State) -> table : SortedMap<'Key, 'Value> -> state : 'State -> 'State + +/// +/// Applies an action to each binding, in the ascending order of the keys. +/// +/// The action. +/// The input sorted map. +/// Thrown when is . +/// +/// O(n). +/// +[] +val iter : action : ('Key -> 'Value -> unit) -> table : SortedMap<'Key, 'Value> -> unit + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input sorted map. +/// +/// The sorted map of the bindings that satisfy the predicate and the sorted map of the other ones, both with the +/// comparers of the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> + table : SortedMap<'Key, 'Value> -> + struct (SortedMap<'Key, 'Value> * SortedMap<'Key, 'Value>) + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the ascending order of the keys. +/// +/// The input sorted map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : SortedMap<'Key, 'Value> -> struct ('Key * 'Value) array + +/// +/// Returns a list of the bindings, in the ascending order of the keys. +/// +/// The input sorted map. +/// The list of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toList : table : SortedMap<'Key, 'Value> -> struct ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the ascending order of the keys. +/// +/// The input sorted map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : SortedMap<'Key, 'Value> -> seq + +/// +/// The functions of that take bindings, with reference tuples +/// instead of key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the shape of the tuples of most F# code has a module of its +/// own (ADR 0001, D13): SortedMap.Tuple.addRange pairs table. The functions resolve after +/// open FSharp.Collections.Immutable.Tuple too, whose SortedMap module has no nested module of this name. +/// +[] +module Tuple = + + // ---- Transformation ---- + + /// + /// Returns a sorted map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input sorted map. + /// The sorted map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the sorted map or of another tuple, and a value that the value + /// comparer finds different. + /// + /// + /// O(m log(n + m)). The function wraps + /// , + /// which rejects another value of a key and accepts a tuple whose value the value comparer finds equal to the value of + /// the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq<'Key * 'Value> -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + + /// + /// Returns a sorted map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input sorted map. + /// The sorted map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)). The function wraps + /// . + /// A tuple whose value the value comparer finds equal to the current value of its key, or to the value of an earlier + /// tuple of the key, leaves that binding as it is, its key included (ADR 0001, D14). + /// + [] + val setItems : pairs : seq<'Key * 'Value> -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// The functions of that take bindings, with struct tuples instead +/// of key/value pairs. +/// +/// +/// A function of the module takes one shape of the bindings, so the struct tuples of ADR 0001, D3 have a module of their own +/// (D13): SortedMap.ValueTuple.setItems pairs table. +/// +[] +module ValueTuple = + + // ---- Transformation ---- + + /// + /// Returns a sorted map with the bindings of key/value tuples added. + /// + /// The key/value tuples to add. + /// The input sorted map. + /// The sorted map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// Thrown when a tuple has the key of a binding of the sorted map or of another tuple, and a value that the value + /// comparer finds different. + /// + /// + /// O(m log(n + m)). The function wraps + /// , + /// which rejects another value of a key and accepts a tuple whose value the value comparer finds equal to the value of + /// the key, leaving that binding as it is, its key included (ADR 0001, D14). + /// + [] + val addRange : pairs : seq -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + + /// + /// Returns a sorted map with the bindings of key/value tuples, which replace the existing bindings of their keys. + /// + /// The key/value tuples to set; a later tuple of a key replaces an earlier one. + /// The input sorted map. + /// The sorted map with the bindings. + /// Thrown when is or a default flat list, or is . + /// + /// O(m log(n + m)). The function wraps + /// . + /// A tuple whose value the value comparer finds equal to the current value of its key, or to the value of an earlier + /// tuple of the key, leaves that binding as it is, its key included (ADR 0001, D14). + /// + [] + val setItems : pairs : seq -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fs new file mode 100644 index 00000000..b9c0825a --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fs @@ -0,0 +1,123 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic +open System.Collections.Immutable + +type SortedMapBuffer<'Key, 'Value when 'Key : not null> = SortedMap<'Key, 'Value>.Builder -> unit + +[] +type SortedMapCE<'Key when 'Key : not null and 'Key : comparison> (keyComparer : IComparer<'Key>) = + + do checkNotNull (nameof keyComparer) keyComparer + + new () = SortedMapCE<'Key>(DefaultComparer<'Key>.Instance) + + member inline _.Zero () : SortedMapBuffer<'Key, 'Value> = ignore + + // Every binding goes through the indexer of the builder, which keeps the earlier binding of a key for a value that the + // value comparer finds equal (ADR 0001, D14) + member inline _.Yield (pair : struct ('Key * 'Value)) : SortedMapBuffer<'Key, 'Value> = + fun builder -> + let struct (key, value) = pair + builder[key] <- value + + member inline _.Yield (pair : KeyValuePair<'Key, 'Value>) : SortedMapBuffer<'Key, 'Value> = + fun builder -> builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (pairs : seq) : SortedMapBuffer<'Key, 'Value> = + // A default flat list given as a sequence fails when it is enumerated; the expression rejects it, as the functions + // of the module do (ADR 0001, D6), when yield! runs and not when the buffer does + checkNotNullOrDefaultSeq (nameof pairs) pairs + + fun builder -> + for struct (key, value) in pairs do + builder[key] <- value + + member inline _.YieldFrom (pairs : struct ('Key * 'Value) array) : SortedMapBuffer<'Key, 'Value> = + // A null array would fail with NullReferenceException when the buffer runs; the expression rejects it by the name of + // the parameter, when yield! runs (ADR 0001, D6) + checkNotNull (nameof pairs) pairs + + fun builder -> + for struct (key, value) in pairs do + builder[key] <- value + + member inline _.YieldFrom (pairs : seq>) : SortedMapBuffer<'Key, 'Value> = + checkNotNullOrDefaultSeq (nameof pairs) pairs + + fun builder -> + for pair in pairs do + builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (table : SortedMap<'Key, 'Value>) : SortedMapBuffer<'Key, 'Value> = + checkNotNull (nameof table) table + + fun builder -> + for pair in table do + builder[pair.Key] <- pair.Value + + member inline _.YieldFrom (buffer : SortedMapBuffer<'Key, 'Value>) : SortedMapBuffer<'Key, 'Value> = buffer + + member inline _.Combine + (first : SortedMapBuffer<'Key, 'Value>, second : SortedMapBuffer<'Key, 'Value>) + : SortedMapBuffer<'Key, 'Value> + = + fun builder -> + first builder + second builder + + member inline _.Delay ([] delayed : unit -> SortedMapBuffer<'Key, 'Value>) : SortedMapBuffer<'Key, 'Value> = + fun builder -> delayed () builder + + member inline _.For + (source : seq<'T>, [] body : 'T -> SortedMapBuffer<'Key, 'Value>) + : SortedMapBuffer<'Key, 'Value> + = + // A default flat list is a sequence that fails when it is enumerated; the expression rejects it as yield! does + checkNotNullOrDefaultSeq (nameof source) source + + fun builder -> + for item in source do + body item builder + + member inline _.While + ([] guard : unit -> bool, body : SortedMapBuffer<'Key, 'Value>) + : SortedMapBuffer<'Key, 'Value> + = + fun builder -> + while guard () do + body builder + + member inline _.TryWith + (body : SortedMapBuffer<'Key, 'Value>, [] handler : exn -> SortedMapBuffer<'Key, 'Value>) + : SortedMapBuffer<'Key, 'Value> + = + fun builder -> + try + body builder + with ex -> + handler ex builder + + member inline _.TryFinally + (body : SortedMapBuffer<'Key, 'Value>, [] compensation : unit -> unit) + : SortedMapBuffer<'Key, 'Value> + = + fun builder -> + try + body builder + finally + compensation () + + member inline _.Using + (resource : 'Resource, [] body : 'Resource -> SortedMapBuffer<'Key, 'Value>) + : SortedMapBuffer<'Key, 'Value> + = + fun builder -> + use resource = resource + body resource builder + + member _.Run (buffer : SortedMapBuffer<'Key, 'Value>) : SortedMap<'Key, 'Value> = + let builder = ImmutableSortedDictionary.CreateBuilder<'Key, 'Value> keyComparer + buffer builder + builder.ToImmutable () diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fsi new file mode 100644 index 00000000..67d3c116 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapCE.fsi @@ -0,0 +1,147 @@ +namespace FSharp.Collections.Immutable + +open System +open System.Collections.Generic + +/// +/// The body of a sortedMap { } expression: a function that sets the bindings in a builder. +/// +type SortedMapBuffer<'Key, 'Value when 'Key : not null> = SortedMap<'Key, 'Value>.Builder -> unit + +/// +/// The builder of the sortedMap { } computation expression. +/// +/// +/// +/// The expression collects its bindings in an +/// (ADR 0001, D7). It yields a binding as +/// a struct pair, yield struct (key, value), or as a . A +/// later binding of a key replaces an earlier one, as in +/// , +/// unless the default comparer of the values finds the two values equal: then the earlier binding stays, its key +/// included, as the indexer of the builder keeps it (ADR 0001, D14). +/// +/// +/// The keys are ordered by the comparer that the constructor takes, or by F# generic comparison, +/// , when it is omitted; the values compare +/// with the default equality comparer. The builder is generic in the type of the keys, so that it can hold a comparer of +/// them, and each sortedMap { } expression infers the type of its keys. The type of the keys must support F# +/// comparison, because the constructor without a comparer orders them with it. +/// SortedMapCE StringComparer.OrdinalIgnoreCase { yield struct ("a", 1) } builds a map whose keys ignore the case. +/// +/// +[] +type SortedMapCE<'Key when 'Key : not null and 'Key : comparison> = + + /// + /// Creates the builder of sorted maps whose keys are ordered by a key comparer. + /// + /// The comparer that orders the keys of the sorted map. + /// Thrown when is . + new : keyComparer : IComparer<'Key> -> SortedMapCE<'Key> + + /// + /// Creates the builder of sorted maps whose keys are ordered by F# generic comparison, + /// . + /// + new : unit -> SortedMapCE<'Key> + + /// An expression without bindings. + member inline Zero : unit -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a struct pair of a key and a value. + /// + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline Yield : pair : struct ('Key * 'Value) -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets a binding given as a key/value pair. + /// + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline Yield : pair : KeyValuePair<'Key, 'Value> -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sequence of struct pairs. + /// + /// Thrown when the sequence is or a default flat list. + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of an array of struct pairs. + /// + /// Thrown when the array is . + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : struct ('Key * 'Value) array -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sequence of key/value pairs, such as another dictionary. + /// + /// Thrown when the sequence is or a default flat list. + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : pairs : seq> -> SortedMapBuffer<'Key, 'Value> + + /// + /// Sets the bindings of a sorted map. + /// + /// Thrown when the sorted map is . + /// + /// A value that the default comparer of the values finds equal to the value of an earlier binding of the key keeps the + /// earlier binding, its key included (ADR 0001, D14). + /// + member inline YieldFrom : table : SortedMap<'Key, 'Value> -> SortedMapBuffer<'Key, 'Value> + + /// Sets the bindings of another expression body. + member inline YieldFrom : buffer : SortedMapBuffer<'Key, 'Value> -> SortedMapBuffer<'Key, 'Value> + + /// Sets the bindings of two parts of the expression, in order. + member inline Combine : + first : SortedMapBuffer<'Key, 'Value> * second : SortedMapBuffer<'Key, 'Value> -> SortedMapBuffer<'Key, 'Value> + + /// + /// Runs the body of the expression each time the bindings are collected. + /// + member inline Delay : [] delayed : (unit -> SortedMapBuffer<'Key, 'Value>) -> SortedMapBuffer<'Key, 'Value> + + /// Sets the bindings of the body for each item of a sequence. + /// Thrown when the sequence is or a default flat list. + member inline For : + source : seq<'T> * [] body : ('T -> SortedMapBuffer<'Key, 'Value>) -> SortedMapBuffer<'Key, 'Value> + + /// Sets the bindings of the body while the guard holds. + member inline While : + [] guard : (unit -> bool) * body : SortedMapBuffer<'Key, 'Value> -> SortedMapBuffer<'Key, 'Value> + + /// Handles the exceptions of the body. + member inline TryWith : + body : SortedMapBuffer<'Key, 'Value> * [] handler : (exn -> SortedMapBuffer<'Key, 'Value>) -> + SortedMapBuffer<'Key, 'Value> + + /// Runs the compensation after the body. + member inline TryFinally : + body : SortedMapBuffer<'Key, 'Value> * [] compensation : (unit -> unit) -> SortedMapBuffer<'Key, 'Value> + + /// Disposes a resource after the body. + member inline Using : + resource : 'Resource * [] body : ('Resource -> SortedMapBuffer<'Key, 'Value>) -> + SortedMapBuffer<'Key, 'Value> + when 'Resource :> IDisposable | null + + /// Collects the bindings of the expression. + member Run : buffer : SortedMapBuffer<'Key, 'Value> -> SortedMap<'Key, 'Value> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fs new file mode 100644 index 00000000..10342839 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fs @@ -0,0 +1,39 @@ +[] +module FSharp.Collections.Immutable.Option.SortedMap + +open FSharp.Collections.Immutable + +// FSharp.Core has no function over an immutable dictionary, so the functions delegate to the default module and convert +// the options at its boundary: the functions that it calls get value options, and its results become options. + +// ---- Access ---- + +[] +let tryFind (key : 'Key) (table : SortedMap<'Key, 'Value>) = SortedMap.tryFind key table |> ValueOption.toOption + +[] +let tryGetKey (key : 'Key) (table : SortedMap<'Key, 'Value>) = SortedMap.tryGetKey key table |> ValueOption.toOption + +// ---- Search ---- + +[] +let pick (chooser : 'Key -> 'Value -> 'U option) (table : SortedMap<'Key, 'Value>) = + SortedMap.pick (fun key value -> chooser key value |> ValueOption.ofOption) table + +[] +let tryFindKey predicate (table : SortedMap<'Key, 'Value>) = SortedMap.tryFindKey predicate table |> ValueOption.toOption + +[] +let tryPick (chooser : 'Key -> 'Value -> 'U option) (table : SortedMap<'Key, 'Value>) = + SortedMap.tryPick (fun key value -> chooser key value |> ValueOption.ofOption) table + |> ValueOption.toOption + +// ---- Transformation ---- + +[] +let change (key : 'Key) (f : 'Value option -> 'Value option) (table : SortedMap<'Key, 'Value>) = + SortedMap.change key (fun current -> f (ValueOption.toOption current) |> ValueOption.ofOption) table + +[] +let choose (chooser : 'Key -> 'Value -> 'U option) (table : SortedMap<'Key, 'Value>) = + SortedMap.choose (fun key value -> chooser key value |> ValueOption.ofOption) table diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fsi new file mode 100644 index 00000000..4a1e3b4c --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapOption.fsi @@ -0,0 +1,120 @@ +/// +/// The functions of that take or return an optional +/// value, with instead of +/// . +/// +/// +/// After open FSharp.Collections.Immutable.Option, these functions shadow the default ones, and the other functions +/// of the default module stay available (ADR 0001, D3). Each function has the signature of the function with the same +/// name in where that module has one. +/// +[] +module FSharp.Collections.Immutable.Option.SortedMap + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the value of a key, or None when the sorted map has no binding of the key. +/// +/// The key to find. +/// The input sorted map. +/// The value of the key, or None. +/// Thrown when is . +/// +/// O(log n). +/// +[] +val tryFind : key : 'Key -> table : SortedMap<'Key, 'Value> -> 'Value option + +/// +/// Returns the key of the sorted map that equals a key in the key comparer, or None. +/// +/// The key to find. +/// The input sorted map. +/// The key that the sorted map holds, which can differ from the given one, or None. +/// Thrown when is . +/// +/// O(log n). The function wraps . +/// +[] +val tryGetKey : key : 'Key -> table : SortedMap<'Key, 'Value> -> 'Key option + +// ---- Search ---- + +/// +/// Returns the first value, in the order of the keys, that the chooser returns for the bindings. +/// +/// The function that returns a value, or None to go on. +/// The input sorted map. +/// The first chosen value. +/// Thrown when is . +/// Thrown when the chooser returns None for every binding. +/// +/// O(n). +/// +[] +val pick : chooser : ('Key -> 'Value -> 'U option) -> table : SortedMap<'Key, 'Value> -> 'U + +/// +/// Returns the key of the first binding, in the order of the keys, that satisfies the predicate, or None. +/// +/// The function to test the bindings. +/// The input sorted map. +/// The key of the first binding that satisfies the predicate, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryFindKey : predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> 'Key option + +/// +/// Returns the first value, in the order of the keys, that the chooser returns for the bindings, or None. +/// +/// The function that returns a value, or None to go on. +/// The input sorted map. +/// The first chosen value, or None. +/// Thrown when is . +/// +/// O(n). +/// +[] +val tryPick : chooser : ('Key -> 'Value -> 'U option) -> table : SortedMap<'Key, 'Value> -> 'U option + +// ---- Transformation ---- + +/// +/// Returns a sorted map with the binding of a key changed by a function of its current value. +/// +/// The key. +/// +/// The function that takes the current value or None, and returns the new value or None to remove the +/// binding. +/// +/// The input sorted map. +/// The sorted map with the changed binding; the input itself when nothing changes. +/// Thrown when is . +/// +/// O(log n). When returns a value that the value comparer of the sorted map finds equal to the +/// current value, the binding stays as it is, its key included, as in +/// (ADR 0001, D14). +/// +[] +val change : key : 'Key -> f : ('Value option -> 'Value option) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> + +/// +/// Returns a sorted map of the values that the chooser returns for the bindings, under their keys. +/// +/// The function that returns the new value, or None to drop the binding. +/// The input sorted map. +/// +/// The sorted map of the chosen values, with the key comparer of the input and the default comparer of the new values. +/// +/// Thrown when is . +/// +/// O(n log n). +/// +[] +val choose : chooser : ('Key -> 'Value -> 'U option) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'U> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fs new file mode 100644 index 00000000..f4358f66 --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fs @@ -0,0 +1,43 @@ +[] +module FSharp.Collections.Immutable.Tuple.SortedMap + +open FSharp.Collections.Immutable + +// The conversions share their algorithm with the default module and only pass another tuple constructor; `maxKeyValue`, +// `minKeyValue` and `partition` delegate to the default module and repack its pair. + +// ---- Access ---- + +[] +let maxKeyValue (table : SortedMap<'Key, 'Value>) = + let struct (key, value) = SortedMap.maxKeyValue table + key, value + +[] +let minKeyValue (table : SortedMap<'Key, 'Value>) = + let struct (key, value) = SortedMap.minKeyValue table + key, value + +// ---- Partitioning ---- + +[] +let partition predicate (table : SortedMap<'Key, 'Value>) = + let struct (accepted, rejected) = SortedMap.partition predicate table + accepted, rejected + +// ---- Conversion ---- + +[] +let toArray (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toArray referencePair table + +[] +let toList (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toList referencePair table + +[] +let toSeq (table : SortedMap<'Key, 'Value>) = + checkNotNull (nameof table) table + Dictionaries.toSeq referencePair table diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fsi b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fsi new file mode 100644 index 00000000..c0c58cdf --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTuple.fsi @@ -0,0 +1,100 @@ +/// +/// 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 . +/// +/// +/// The functions that take bindings take every shape without this module (D13): SortedMap.ofSeq is overloaded for +/// reference tuples, and the nested module of the default +/// module holds the functions that change a map by reference tuples. +/// +/// +[] +module FSharp.Collections.Immutable.Tuple.SortedMap + +open FSharp.Collections.Immutable + +// ---- Access ---- + +/// +/// Returns the binding of the largest key of the sorted map. +/// +/// The input sorted map. +/// The key and the value of the last binding in the order of the keys. +/// Thrown when is . +/// Thrown when the sorted map is empty. +/// +/// O(n), where +/// +/// takes O(log n): enumerates its bindings only +/// in ascending order and gives no other access to its largest key, so the function visits every binding. +/// +[] +val maxKeyValue : table : SortedMap<'Key, 'Value> -> 'Key * 'Value + +/// Returns the binding of the smallest key of the sorted map. +/// The input sorted map. +/// The key and the value of the first binding in the order of the keys. +/// Thrown when is . +/// Thrown when the sorted map is empty. +/// O(log n). +[] +val minKeyValue : table : SortedMap<'Key, 'Value> -> 'Key * 'Value + +// ---- Partitioning ---- + +/// Splits the bindings by a predicate. +/// The function to test the bindings. +/// The input sorted map. +/// +/// The sorted map of the bindings that satisfy the predicate and the sorted map of the other ones, both with the +/// comparers of the input. +/// +/// Thrown when is . +/// O(n log n). +[] +val partition : + predicate : ('Key -> 'Value -> bool) -> table : SortedMap<'Key, 'Value> -> SortedMap<'Key, 'Value> * SortedMap<'Key, 'Value> + +// ---- Conversion ---- + +/// +/// Returns a new array of the bindings, in the ascending order of the keys. +/// +/// The input sorted map. +/// The array of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toArray : table : SortedMap<'Key, 'Value> -> ('Key * 'Value) array + +/// +/// Returns a list of the bindings, in the ascending order of the keys. +/// +/// The input sorted map. +/// The list of key/value pairs. +/// Thrown when is . +/// +/// O(n). +/// +[] +val toList : table : SortedMap<'Key, 'Value> -> ('Key * 'Value) list + +/// +/// Returns the bindings as a sequence, in the ascending order of the keys. +/// +/// The input sorted map. +/// The sequence of key/value pairs, computed on demand. +/// Thrown when is . +/// +/// O(1); O(n) to enumerate. +/// +[] +val toSeq : table : SortedMap<'Key, 'Value> -> seq<'Key * 'Value> diff --git a/src/FSharp.Collections.Immutable/SortedMap/SortedMapTypes.fs b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTypes.fs new file mode 100644 index 00000000..9e697e5b --- /dev/null +++ b/src/FSharp.Collections.Immutable/SortedMap/SortedMapTypes.fs @@ -0,0 +1,15 @@ +namespace FSharp.Collections.Immutable + +open System.Collections.Immutable + +// The abbreviation has its own file because the modules of the folder are root modules, which cannot declare a type of +// the namespace; a file of abbreviations needs no signature file (ADR 0001, D2). + +/// +/// An immutable sorted map, . +/// +/// +/// The map keeps its bindings in a balanced binary tree that a key comparer orders, and enumerates them in the ascending +/// order of the keys. +/// +type SortedMap<'Key, 'Value when 'Key : not null> = ImmutableSortedDictionary<'Key, 'Value> 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 4669bd86..a973f6da 100644 --- a/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj +++ b/tests/FSharp.Collections.Immutable.Tests/FSharp.Collections.Immutable.Tests.fsproj @@ -83,6 +83,15 @@ + + + + + + + + + diff --git a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs index a59b2135..0b0e8d62 100644 --- a/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs +++ b/tests/FSharp.Collections.Immutable.Tests/HashMap/IMapTests.fs @@ -540,3 +540,40 @@ type IMapTests () = "IMap.containsBinding must throw for a null key of a sorted map, as ImmutableSortedDictionary.Contains does" ) |> ignore + + [] + member _.``filter keeps the implementation, the comparers and the order of a SortedMap`` () = + let valueComparer = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + + // The keys are ordered by F# generic comparison, which puts the upper case letters before the lower case ones + let table = + (SortedMap.forComparers LanguagePrimitives.FastGenericComparer valueComparer).ofArray [| + struct ("b", 1) + struct ("a", 2) + struct ("B", 3) + struct ("A", 4) + |] + :> IMap + + match IMap.filter (fun _ value -> value > 1) table with + | :? SortedMap as filtered -> + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer filtered, + "IMap.filter must keep the key comparer of a sorted map" + ) + + Assert.AreSame ( + valueComparer, + SortedMap.valueComparer filtered, + "IMap.filter must keep the value comparer of a sorted map" + ) + + CollectionAssert.AreEqual ( + [| "A"; "B"; "a" |], + Seq.toArray (SortedMap.keys filtered), + "IMap.filter must keep the order of the key comparer of a sorted map" + ) + | filtered -> + Assert.Fail $"IMap.filter must keep the implementation of a sorted map, not return %s{filtered.GetType().Name}" diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/CustomComparersSortedMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/CustomComparersSortedMapTests.fs new file mode 100644 index 00000000..f27a0b4a --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/CustomComparersSortedMapTests.fs @@ -0,0 +1,613 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The comparer struct of `SortedMap`, `CustomComparersSortedMap`, and the functions of the module that create it (ADR 0001, +/// D12). The creation contract runs its members with a key comparer; these tests cover the comparers themselves. +[] +type CustomComparersSortedMapTests () = + + /// A key comparer that differs from the default one: it orders strings ordinally from the last to the first. + static let reverseOrdinal = + ComparisonIdentity.FromFunction (fun (left : string) (right : string) -> String.CompareOrdinal (right, left)) + + /// A key comparer that orders integers from the largest to the smallest. + static let descending = ComparisonIdentity.FromFunction (fun (left : int) (right : int) -> compare right left) + + /// A value comparer that differs from the default one: it compares strings by their length. + static let byLength = + HashIdentity.FromFunctions (fun (value : string) -> value.Length) (fun left right -> left.Length = right.Length) + + /// A value comparer of numbers that differs from the default one: it finds numbers of the same absolute value equal. + static let absoluteValues = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + + /// The struct whose key comparer ignores the case of the keys. + static let ignoreCase : CustomComparersSortedMap = + SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + static let nullKeyComparer = Unchecked.defaultof> + + static let nullValueComparer = Unchecked.defaultof> + + /// The key comparer and the value comparer of a row of the tests of null comparers: the comparer whose parameter the + /// row names is null. + static let comparersWithNull (nullParameter : string) : struct (IComparer * IEqualityComparer) = + match nullParameter with + | "keyComparer" -> struct (nullKeyComparer, byLength) + | _ -> struct (StringComparer.Ordinal, nullValueComparer) + + /// The members of the struct that create a sorted map of the keys "b", "a" and "c", each bound to itself, by name. + static let creations : Map -> SortedMap> = + Map.ofArray [| + "empty", + (fun comparers -> + comparers.empty + |> SortedMap.add "b" "b" + |> SortedMap.add "a" "a" + |> SortedMap.add "c" "c" + ) + "ofArray of key/value pairs", + (fun comparers -> comparers.ofArray [| KeyValuePair ("b", "b"); KeyValuePair ("a", "a"); KeyValuePair ("c", "c") |]) + "ofArray of reference tuples", (fun comparers -> comparers.ofArray [| "b", "b"; "a", "a"; "c", "c" |]) + "ofArray of struct tuples", + (fun comparers -> comparers.ofArray [| struct ("b", "b"); struct ("a", "a"); struct ("c", "c") |]) + "ofList of key/value pairs", + (fun comparers -> comparers.ofList [ KeyValuePair ("b", "b"); KeyValuePair ("a", "a"); KeyValuePair ("c", "c") ]) + "ofList of reference tuples", (fun comparers -> comparers.ofList [ "b", "b"; "a", "a"; "c", "c" ]) + "ofList of struct tuples", + (fun comparers -> comparers.ofList [ struct ("b", "b"); struct ("a", "a"); struct ("c", "c") ]) + "ofSeq of key/value pairs", (fun comparers -> comparers.ofSeq (dict [| "b", "b"; "a", "a"; "c", "c" |])) + "ofSeq of reference tuples", (fun comparers -> comparers.ofSeq (Seq.ofArray [| "b", "b"; "a", "a"; "c", "c" |])) + "ofSeq of struct tuples", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| struct ("b", "b"); struct ("a", "a"); struct ("c", "c") |])) + "ofSeqBy", (fun comparers -> comparers.ofSeqBy id [| "b"; "a"; "c" |]) + "singleton", + (fun comparers -> + comparers.singleton "b" "b" + |> SortedMap.add "a" "a" + |> SortedMap.add "c" "c" + ) + "build", + (fun comparers -> + comparers.build (fun builder -> + builder["b"] <- "b" + builder["a"] <- "a" + builder["c"] <- "c" + ) + ) + "builder", + (fun comparers -> + let builder = comparers.builder () + builder["b"] <- "b" + builder["a"] <- "a" + builder["c"] <- "c" + builder.ToImmutable () + ) + |] + + /// The creations of a sorted map whose keys ignore the case from the bindings of "A" to 3, "B" to 2, "a" to 1 and "b" to + /// 4, in this order, by the member and the source that they take. The keys "a" and "b" come later than "A" and "B" in + /// every source, F# generic comparison included, so that their bindings are the last ones of their keys. + static let ignoreCaseCreations : Map SortedMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun () -> + ignoreCase.ofArray [| + KeyValuePair ("A", 3) + KeyValuePair ("B", 2) + KeyValuePair ("a", 1) + KeyValuePair ("b", 4) + |] + ) + "ofArray of reference tuples", (fun () -> ignoreCase.ofArray [| "A", 3; "B", 2; "a", 1; "b", 4 |]) + "ofArray of struct tuples", + (fun () -> ignoreCase.ofArray [| struct ("A", 3); struct ("B", 2); struct ("a", 1); struct ("b", 4) |]) + "ofList of key/value pairs", + (fun () -> + ignoreCase.ofList [ KeyValuePair ("A", 3); KeyValuePair ("B", 2); KeyValuePair ("a", 1); KeyValuePair ("b", 4) ] + ) + "ofList of reference tuples", (fun () -> ignoreCase.ofList [ "A", 3; "B", 2; "a", 1; "b", 4 ]) + "ofList of struct tuples", + (fun () -> ignoreCase.ofList [ struct ("A", 3); struct ("B", 2); struct ("a", 1); struct ("b", 4) ]) + "ofSeq of key/value pairs", + (fun () -> + ignoreCase.ofSeq ( + Seq.ofArray [| KeyValuePair ("A", 3); KeyValuePair ("B", 2); KeyValuePair ("a", 1); KeyValuePair ("b", 4) |] + ) + ) + "ofSeq of reference tuples", (fun () -> ignoreCase.ofSeq (Seq.ofArray [| "A", 3; "B", 2; "a", 1; "b", 4 |])) + "ofSeq of struct tuples", + (fun () -> ignoreCase.ofSeq (Seq.ofArray [| struct ("A", 3); struct ("B", 2); struct ("a", 1); struct ("b", 4) |])) + "ofSeq of a dictionary", (fun () -> ignoreCase.ofSeq (dict [| "A", 3; "B", 2; "a", 1; "b", 4 |])) + "ofSeq of an F# map", (fun () -> ignoreCase.ofSeq (Map.ofArray [| "a", 1; "b", 4; "A", 3; "B", 2 |])) + // The sorted map has another key comparer, so ofSeq gives its bindings the comparer of the struct one by one + "ofSeq of a sorted map", (fun () -> ignoreCase.ofSeq (SortedMap.ofArray [| "a", 1; "b", 4; "A", 3; "B", 2 |])) + |] + + /// The members of the struct that create a sorted map from the bindings of "one" to "uno" and of "ONE" to "abc", by + /// name: keys that ignore the case and values that compare by their length make the two bindings equal. + static let equalValueCreations : Map -> SortedMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun comparers -> comparers.ofArray [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |]) + "ofArray of reference tuples", (fun comparers -> comparers.ofArray [| "one", "uno"; "ONE", "abc" |]) + "ofArray of struct tuples", (fun comparers -> comparers.ofArray [| struct ("one", "uno"); struct ("ONE", "abc") |]) + "ofList of key/value pairs", + (fun comparers -> comparers.ofList [ KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") ]) + "ofList of reference tuples", (fun comparers -> comparers.ofList [ "one", "uno"; "ONE", "abc" ]) + "ofList of struct tuples", (fun comparers -> comparers.ofList [ struct ("one", "uno"); struct ("ONE", "abc") ]) + "ofSeq of key/value pairs", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| KeyValuePair ("one", "uno"); KeyValuePair ("ONE", "abc") |])) + "ofSeq of reference tuples", (fun comparers -> comparers.ofSeq (Seq.ofArray [| "one", "uno"; "ONE", "abc" |])) + "ofSeq of struct tuples", + (fun comparers -> comparers.ofSeq (Seq.ofArray [| struct ("one", "uno"); struct ("ONE", "abc") |])) + "ofSeqBy", + (fun comparers -> + comparers.ofSeqBy + (fun value -> + if String.Equals (value, "uno", StringComparison.Ordinal) then + "one" + else + "ONE" + ) + [| "uno"; "abc" |] + ) + |] + + /// The members of the struct that take a sequence, by name, with the name of the sequence parameter and the member on a + /// null sequence. The struct has comparers that differ from the default ones. + static let nullSequenceMembers : Map unit))> = + let comparers = SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength + + Map.ofArray [| + "ofSeq of key/value pairs", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>>) + |> ignore) + "ofSeq of reference tuples", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeq of struct tuples", + struct ("elements", + fun () -> + comparers.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeqBy", + struct ("source", + fun () -> + comparers.ofSeqBy id (Unchecked.defaultof>) + |> ignore) + |] + + /// The members of the struct that take a sequence, by name, with the name of the sequence parameter and the member on a + /// default flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails + /// with InvalidOperationException when the sequence is enumerated. The last row gives a sequence of objects, which a + /// default flat list of strings is by the covariance of seq: the element type of the sequence is not the one of the + /// flat list. + static let defaultSequenceMembers : Map unit))> = + let comparers = SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength + + let objectComparers : CustomComparersSortedMap = + SortedMap.forKeyComparer Comparer.Default + + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultValues = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + "ofSeq of key/value pairs", struct ("elements", fun () -> comparers.ofSeq defaultPairs |> ignore) + "ofSeq of reference tuples", struct ("elements", fun () -> comparers.ofSeq defaultTuples |> ignore) + "ofSeq of struct tuples", struct ("elements", fun () -> comparers.ofSeq defaultStructTuples |> ignore) + "ofSeqBy", struct ("source", fun () -> comparers.ofSeqBy id defaultValues |> ignore) + "ofSeqBy (default flat list of strings as objects)", + struct ("source", + fun () -> + objectComparers.ofSeqBy id FlatListOracles.defaultStringsAsObjects + |> ignore) + |] + + /// The names of the members that take a sequence and are given a null one, as the rows of their test. + static member NullSequenceMembers = nullSequenceMembers.Keys + + /// The names of the members that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceMembers = defaultSequenceMembers.Keys + + /// The names of the members that create a sorted map, as the rows of the test of their comparers. + static member Creations = creations.Keys + + /// The names of the creations from bindings whose keys the comparer of the struct considers equal. + static member IgnoreCaseCreations = ignoreCaseCreations.Keys + + /// The names of the members that create a sorted map from bindings that the comparers find equal, as the rows of their + /// test. + static member EqualValueCreations = equalValueCreations.Keys + + /// The keys of the sorted maps that the tests of an existing map give to ofSeq. The property builds new arrays on each + /// read. + static member Keys : int[] seq = seq { + [||] + [| 42 |] + [| 5; -3; 8; 0; 12 |] + } + + [] + member _.``the constructor with both comparers rejects a null comparer`` (nullParameter : string) = + let struct (keyComparer, valueComparer) = comparersWithNull nullParameter + + let ex = + Assert.ThrowsExactly( + Action (fun () -> + CustomComparersSortedMap(keyComparer, valueComparer) + |> ignore + ), + "The constructor must reject a null comparer" + ) + + Assert.AreEqual(nullParameter, ex.ParamName, "The constructor must name the null comparer") + + [] + member _.``the constructor with the key comparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + CustomComparersSortedMap nullKeyComparer + |> ignore + ), + "The constructor must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "The constructor must name the key comparer") + + [] + member _.``forComparers rejects a null comparer`` (nullParameter : string) = + let struct (keyComparer, valueComparer) = comparersWithNull nullParameter + + let ex = + Assert.ThrowsExactly( + Action (fun () -> SortedMap.forComparers keyComparer valueComparer |> ignore), + "SortedMap.forComparers must reject a null comparer" + ) + + Assert.AreEqual(nullParameter, ex.ParamName, "SortedMap.forComparers must name the null comparer") + + [] + member _.``forKeyComparer rejects a null comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + (SortedMap.forKeyComparer nullKeyComparer : CustomComparersSortedMap) + |> ignore + ), + "SortedMap.forKeyComparer must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "SortedMap.forKeyComparer must name the key comparer") + + [] + member _.``members reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceMembers[name] + FlatListAssert.RejectsNull ($"CustomComparersSortedMap.%s{name}", paramName, call) + + [] + member _.``members reject a default flat list given as a sequence`` (name : string) = + // The exception names the parameter of the member, not the one of the BCL member that it would have called + let struct (paramName, call) = defaultSequenceMembers[name] + FlatListAssert.RejectsNull ($"CustomComparersSortedMap.%s{name}", paramName, call) + + [] + member _.``members take a flat list that is not default as a sequence, also one of strings as a sequence of objects`` () = + let flatList (elements : 'T array) = FlatList.ofArray elements :> seq<'T> + let comparers = SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (comparers.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (comparers.ofSeq (flatList [| ("a", "b") |])), + "ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (comparers.ofSeq (flatList [| struct ("a", "b") |])), + "ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + SortedMap.toArray (comparers.ofSeqBy id (flatList [| "ab" |])), + "ofSeqBy must take the elements of a flat list" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let objectComparers : CustomComparersSortedMap = + SortedMap.forKeyComparer Comparer.Default + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "ab" |]) + + CollectionAssert.AreEqual ( + [| "ab" :> obj |], + SortedMap.keys (objectComparers.ofSeqBy id widened) + |> Seq.toArray, + "ofSeqBy must take the elements of a flat list of strings as objects" + ) + + [] + member _.``forComparers holds both comparers`` () = + let comparers = SortedMap.forComparers reverseOrdinal byLength + Assert.AreSame (reverseOrdinal, comparers.KeyComparer, "SortedMap.forComparers must hold the key comparer") + Assert.AreSame (byLength, comparers.ValueComparer, "SortedMap.forComparers must hold the value comparer") + + [] + member _.``forKeyComparer holds the key comparer and the default comparer of the values`` () = + let comparers : CustomComparersSortedMap = SortedMap.forKeyComparer reverseOrdinal + Assert.AreSame (reverseOrdinal, comparers.KeyComparer, "SortedMap.forKeyComparer must hold the key comparer") + + Assert.AreSame>( + EqualityComparer.Default, + comparers.ValueComparer, + "SortedMap.forKeyComparer must hold the default comparer of the values" + ) + + [] + member _.``forKeyComparer StringComparer.OrdinalIgnoreCase applies the comparer`` () = + let table = + (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq [| "two", 2; "one", 1; "ONE", 3 |] + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + SortedMap.keyComparer table, + "The sorted map of SortedMap.forKeyComparer must have the key comparer" + ) + + CollectionAssert.AreEqual ( + [| struct ("ONE", 3); struct ("two", 2) |], + SortedMap.toArray table, + "The comparer must merge the keys that differ in their case into the last binding of them, in its order" + ) + + Assert.AreEqual(2, SortedMap.find "TWO" table, "The sorted map must look the keys up with the comparer") + + [] + member _.``every member creates sorted maps with the comparers of the struct`` (creation : string) = + let table = creations[creation](SortedMap.forComparers reverseOrdinal byLength) + + Assert.AreSame ( + reverseOrdinal, + SortedMap.keyComparer table, + "The member must create a sorted map with the key comparer of the struct" + ) + + Assert.AreSame ( + byLength, + SortedMap.valueComparer table, + "The member must create a sorted map with the value comparer of the struct" + ) + + CollectionAssert.AreEqual ( + [| "c"; "b"; "a" |], + Seq.toArray (SortedMap.keys table), + "The member must order the keys with the key comparer of the struct" + ) + + [] + member _.``every source keeps the last binding of the keys that the comparer considers equal`` (creation : string) = + CollectionAssert.AreEqual ( + [| struct ("a", 1); struct ("b", 4) |], + SortedMap.toArray (ignoreCaseCreations[creation]()), + "The sorted map must keep the last binding of the keys that differ in their case, as Map keeps the last binding of a key" + ) + + [] + member _.``every member keeps the earlier binding of a key for a value that the value comparer finds equal`` + (creation : string) + = + // The indexer of the builder and ToImmutableSortedDictionary keep the earlier binding, its key included (ADR 0001, + // D14) + let table = + equalValueCreations[creation](SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength) + + let struct (key, value) = + Assert.ContainsSingle (SortedMap.toArray table, "The member must merge the bindings of the equal keys") + + Assert.AreEqual("one", key, "The member must keep the earlier key") + Assert.AreEqual("uno", value, "The member must keep the earlier value") + + [] + member _.``the default value of the struct stands for the comparers that the BCL takes for null ones`` () = + let comparers = CustomComparersSortedMap() + let words = [| "b"; "a"; "B"; "A"; "_"; "10"; "9" |] + let table = comparers.ofArray (words |> Array.map (fun word -> word, word)) + + Assert.AreSame>( + Comparer.Default, + comparers.KeyComparer, + "The default value of the struct must report the key comparer of the BCL" + ) + + Assert.AreSame>( + EqualityComparer.Default, + comparers.ValueComparer, + "The default value of the struct must report the default value comparer" + ) + + Assert.AreSame>( + Comparer.Default, + SortedMap.keyComparer table, + "The default value of the struct must create sorted maps with the key comparer of the BCL" + ) + + // The order of the current culture, which differs from the ordinal order of F# generic comparison + CollectionAssert.AreEqual ( + Array.sortWith (fun left right -> Comparer.Default.Compare(left, right)) words, + Seq.toArray (SortedMap.keys table), + "The default value of the struct must order the keys as Comparer.Default does" + ) + + [] + member _.``ofSeq returns a sorted map with the comparers of the struct itself`` (keys : int array) = + let comparers : CustomComparersSortedMap = SortedMap.forKeyComparer descending + let table = comparers.ofArray (keys |> Array.map (fun key -> struct (key, string key))) + Assert.AreSame (table, comparers.ofSeq table, "ofSeq must return a sorted map with the comparers of the struct itself") + + [] + member _.``ofSeq keeps a sorted map with the key comparer of the struct and gives it the value comparer`` (keys : int array) = + let table = + (SortedMap.forKeyComparer descending).ofArray(keys |> Array.map (fun key -> struct (key, string key))) + let withValueComparer = (SortedMap.forComparers descending byLength).ofSeq table + Assert.AreSame (descending, SortedMap.keyComparer withValueComparer, "ofSeq must keep the key comparer of the struct") + Assert.AreSame (byLength, SortedMap.valueComparer withValueComparer, "ofSeq must give the sorted map the value comparer") + + CollectionAssert.AreEqual ( + SortedMap.toArray table, + SortedMap.toArray withValueComparer, + "ofSeq must keep the bindings of a sorted map with the key comparer of the struct, in their order" + ) + + [] + member _.``ofSeq keeps the first of the keys that become equal when the value comparer finds their values equal`` () = + // "uno" and "abc" have the same length, so the earlier binding in the order of enumeration stays, its key included + // (ADR 0001, D14). The sorted map orders "A" before "a", so "A" is the first of the two keys. + let table = SortedMap.ofArray [| "a", "uno"; "A", "abc"; "b", "dos" |] + + let struct (firstKey, firstValue) = + SortedMap.toArray table + |> Array.find (fun struct (key, _) -> String.Equals (key, "a", StringComparison.OrdinalIgnoreCase)) + + let ignoreCase = (SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofSeq table + + Assert.AreEqual( + 2, + SortedMap.count ignoreCase, + "ofSeq must merge the keys that the comparer of the struct makes equal" + ) + + Assert.AreEqual( + ValueSome firstKey, + SortedMap.tryGetKey "a" ignoreCase, + "ofSeq must keep the first of the equal keys in the order of enumeration" + ) + + Assert.AreEqual( + firstValue, + SortedMap.find "a" ignoreCase, + "ofSeq must keep the value of the first of the equal keys" + ) + + [] + member _.``ofSeq rebuilds a sorted map whose key comparer is another instance, even when it is equivalent`` () = + let ordinal () = + ComparisonIdentity.FromFunction (fun (left : string) right -> String.CompareOrdinal (left, right)) + + let table = (SortedMap.forKeyComparer (ordinal ())).ofArray [| "a", 1; "b", 2 |] + let equivalent = ordinal () + let result = (SortedMap.forKeyComparer equivalent).ofSeq table + + Assert.AreNotSame (table, result, "ofSeq must rebuild a sorted map whose key comparer is another instance") + Assert.AreSame (equivalent, SortedMap.keyComparer result, "ofSeq must give the bindings the key comparer of the struct") + + CollectionAssert.AreEqual ( + SortedMap.toArray table, + SortedMap.toArray result, + "ofSeq must keep the bindings of a sorted map whose key comparer is equivalent" + ) + + [] + member _.``ofSeq gives a sorted map of the module another value comparer through its own key comparer`` () = + // The sorted maps of the module order their keys with F# generic comparison: "A" comes before "a" + let table = SortedMap.ofArray [| "b", 2; "a", 1; "A", 0 |] + let keyComparer = SortedMap.keyComparer table + + Assert.AreSame ( + table, + (SortedMap.forKeyComparer keyComparer).ofSeq table, + "ofSeq must return a sorted map with the key comparer and the value comparer of the struct itself" + ) + + let withValueComparer = (SortedMap.forComparers keyComparer absoluteValues).ofSeq table + Assert.AreSame (keyComparer, SortedMap.keyComparer withValueComparer, "ofSeq must keep the key comparer") + Assert.AreSame (absoluteValues, SortedMap.valueComparer withValueComparer, "ofSeq must give the value comparer") + + CollectionAssert.AreEqual ( + SortedMap.toArray table, + SortedMap.toArray withValueComparer, + "ofSeq must keep the bindings of the sorted map, in their order" + ) + + [] + member _.``ofSeq merges the keys that the key comparer of the struct makes equal where WithComparers throws`` () = + // The sorted map orders "A" before "a", so "a" is the later binding of the two keys + let table = SortedMap.ofArray [| "a", 1; "A", 2 |] + let merged = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table + + CollectionAssert.AreEqual ( + [| struct ("a", 1) |], + SortedMap.toArray merged, + "ofSeq must merge the keys into the later binding in the order of enumeration" + ) + + Assert.ThrowsExactly( + Action (fun () -> + table.WithComparers StringComparer.OrdinalIgnoreCase + |> ignore + ), + "WithComparers must reject keys that become equal with another value, which ofSeq merges" + ) + |> ignore + + [] + member _.``a struct from forKeyComparer replaces the value comparer of the sorted map where WithComparers keeps it`` () = + let table = (SortedMap.forComparers StringComparer.Ordinal absoluteValues).ofArray [| "a", 1 |] + + Assert.AreSame>( + EqualityComparer.Default, + SortedMap.valueComparer ((SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofSeq table), + "ofSeq of a struct from forKeyComparer must give the default comparer of the values" + ) + + Assert.AreSame ( + absoluteValues, + SortedMap.valueComparer ( + (SortedMap.forComparers StringComparer.OrdinalIgnoreCase (SortedMap.valueComparer table)).ofSeq table + ), + "ofSeq of a struct from forComparers with the value comparer of the map must keep it" + ) + + Assert.AreSame ( + absoluteValues, + table.WithComparers(StringComparer.OrdinalIgnoreCase).ValueComparer, + "WithComparers with a key comparer must keep the value comparer" + ) + + [] + member _.``ofSeqBy compares the keys and the values with the comparers of the struct`` () = + let comparers = SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength + // The two keys differ only in their case, so that the key comparer of the struct makes them equal + let projection (value : string) = + if String.Equals (value, "one", StringComparison.Ordinal) then + "key" + else + "KEY" + + Assert.AreEqual( + 1, + SortedMap.count (comparers.ofSeqBy projection [| "one"; "two" |]), + "ofSeqBy must accept equal keys whose values are equal in the value comparer, as the BCL does" + ) + + Assert.ThrowsExactly( + Action (fun () -> comparers.ofSeqBy projection [| "one"; "three" |] |> ignore), + "ofSeqBy must reject equal keys whose values differ in the value comparer, as the BCL does" + ) + |> ignore diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapBuilderTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapBuilderTests.fs new file mode 100644 index 00000000..ab968699 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapBuilderTests.fs @@ -0,0 +1,242 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The builder functions of `SortedMap`; maps have no builder module (ADR 0001, D2). +[] +type SortedMapBuilderTests () = + + /// A value comparer that differs from the default one: it finds numbers of the same absolute value equal. + static let absoluteValues = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + + [] + member _.``build returns the bindings that the action sets in a builder with the default comparers`` () = + let table = + SortedMap.build (fun builder -> + for i in 3..-1..1 do + builder[i] <- i * i + ) + + CollectionAssert.AreEqual ( + [| struct (1, 1); struct (2, 4); struct (3, 9) |], + SortedMap.toArray table, + "SortedMap.build must return the bindings that the action set, in the order of the keys" + ) + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "SortedMap.build must order the keys with F# generic comparison" + ) + + Assert.AreSame>( + EqualityComparer.Default, + SortedMap.valueComparer table, + "SortedMap.build must compare the values with the default comparer" + ) + + [] + member _.``build returns an empty map for an action that sets nothing`` () = + let table : SortedMap = SortedMap.build ignore + Assert.IsTrue (SortedMap.isEmpty table, "SortedMap.build must return an empty map when the action sets nothing") + + [] + member _.``build keeps the earlier binding of a key for a value that the default comparer finds equal`` () = + // The indexer of the builder leaves the binding as it is, its key included (ADR 0001, D14) + let first = MapInputs.Copy "uno" + + let table = + SortedMap.build (fun builder -> + builder["one"] <- first + builder["one"] <- MapInputs.Copy "uno" + ) + + Assert.AreSame (first, SortedMap.find "one" table, "SortedMap.build must keep the earlier value of the key") + + [] + member _.``the build of the comparer struct has both comparers and merges the equal bindings`` () = + let comparers = SortedMap.forComparers StringComparer.OrdinalIgnoreCase absoluteValues + + let table = + comparers.build (fun builder -> + builder["one"] <- 1 + builder["ONE"] <- -1 + ) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + SortedMap.keyComparer table, + "The build of the struct must create a sorted map with the key comparer" + ) + + Assert.AreSame ( + absoluteValues, + SortedMap.valueComparer table, + "The build of the struct must create a sorted map with the value comparer" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", 1) |], + SortedMap.toArray table, + "The build of the struct must keep the earlier binding for a value that the value comparer finds equal" + ) + + [] + member _.``update applies the action to a builder over the bindings and keeps the comparers`` () = + let table = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + let updated = + SortedMap.update + (fun builder -> + builder["THREE"] <- 3 + builder.Remove "ONE" |> ignore + ) + table + + CollectionAssert.AreEqual ( + [| struct ("THREE", 3); struct ("two", 2) |], + SortedMap.toArray updated, + "SortedMap.update must apply the action to the bindings of the map" + ) + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + SortedMap.keyComparer updated, + "SortedMap.update must keep the key comparer" + ) + + Assert.AreEqual(2, SortedMap.count table, "SortedMap.update must not change the input") + Assert.IsTrue (SortedMap.containsKey "one" table, "SortedMap.update must not change the input") + + [] + member _.``builder starts empty with F# generic comparison and the default value comparer`` () = + let builder = SortedMap.builder() + Assert.AreEqual(0, builder.Count, "SortedMap.builder must start without bindings") + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + builder.KeyComparer, + "SortedMap.builder must order the keys with F# generic comparison" + ) + + Assert.AreSame>( + EqualityComparer.Default, + builder.ValueComparer, + "SortedMap.builder must compare the values with the default comparer" + ) + + [] + member _.``builder orders the keys as Map does`` () = + let builder = SortedMap.builder () + + for key in [| "b"; "a"; "B"; "A"; "_" |] do + builder[key] <- key.Length + + CollectionAssert.AreEqual ( + [| "A"; "B"; "_"; "a"; "b" |], + Seq.toArray (SortedMap.keys (SortedMap.ofBuilder builder)), + "SortedMap.builder must order the keys ordinally, as Map does" + ) + + [] + member _.``the builder of the comparer struct has both comparers`` () = + let valueComparer = + HashIdentity.FromFunctions (fun (value : int) -> abs value) (fun left right -> abs left = abs right) + + let builder = (SortedMap.forComparers StringComparer.OrdinalIgnoreCase valueComparer).builder() + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + builder.KeyComparer, + "The builder of SortedMap.forComparers must have the key comparer" + ) + + Assert.AreSame ( + valueComparer, + builder.ValueComparer, + "The builder of SortedMap.forComparers must have the value comparer" + ) + + [] + member _.``the builder of a comparer struct with a key comparer has the default comparer of the values`` () = + let builder : SortedMap.Builder = + (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).builder() + + builder["one"] <- 1 + builder["ONE"] <- 2 + Assert.AreEqual(1, builder.Count, "The builder of SortedMap.forKeyComparer must look the keys up with the comparer") + + Assert.AreSame>( + EqualityComparer.Default, + builder.ValueComparer, + "The builder of SortedMap.forKeyComparer must compare the values with the default comparer" + ) + + [] + member _.``ofBuilder returns the bindings and keeps the builder usable`` () = + let builder : SortedMap.Builder = + (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).builder() + + builder["one"] <- 1 + let table = SortedMap.ofBuilder builder + builder["two"] <- 2 + + CollectionAssert.AreEqual ( + [| struct ("one", 1) |], + SortedMap.toArray table, + "SortedMap.ofBuilder must return the bindings" + ) + + Assert.AreEqual(2, builder.Count, "SortedMap.ofBuilder must keep the builder usable") + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + SortedMap.keyComparer table, + "SortedMap.ofBuilder must keep the comparers" + ) + + [] + member _.``ofBuilder throws for a null builder`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + SortedMap.ofBuilder (Unchecked.defaultof.Builder>) + |> ignore + ), + "SortedMap.ofBuilder must reject a null builder" + ) + + Assert.AreEqual("builder", ex.ParamName, "SortedMap.ofBuilder must name the builder parameter") + + [] + member _.``toBuilder holds the bindings and leaves the map unchanged`` () = + let table = (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "two", 2 |] + + let builder = SortedMap.toBuilder table + builder["THREE"] <- 3 + builder.Remove "ONE" |> ignore + + Assert.AreEqual(2, builder.Count, "SortedMap.toBuilder must hold the bindings and use the key comparer") + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + builder.KeyComparer, + "SortedMap.toBuilder must keep the key comparer" + ) + + Assert.AreEqual(2, SortedMap.count table, "SortedMap.toBuilder must not change the map") + Assert.IsTrue (SortedMap.containsKey "one" table, "SortedMap.toBuilder must not change the map") + + [] + member _.``a builder of a map returns the map itself when nothing changed`` () = + let table = SortedMap.ofArray [| struct ("one", 1) |] + + Assert.AreSame ( + table, + SortedMap.ofBuilder (SortedMap.toBuilder table), + "A builder that changed nothing must return its map" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapComputationExpressionTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapComputationExpressionTests.fs new file mode 100644 index 00000000..f20fd434 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapComputationExpressionTests.fs @@ -0,0 +1,442 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The `sortedMap { }` computation expression. +[] +type SortedMapComputationExpressionTests () = + + /// The expressions whose keys ignore the case and that bind "one" to the first value and then "ONE" to the second one, + /// by the member that sets the second binding. + static let equalValueExpressions : Map string -> SortedMap> = + let ignoreCase = SortedMapCE StringComparer.OrdinalIgnoreCase + + Map.ofArray [| + "yield of a struct pair", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield struct ("ONE", second) + }) + "yield of a key/value pair", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield KeyValuePair ("ONE", second) + }) + "yield! of a sequence of struct pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! Seq.singleton (struct ("ONE", second)) + }) + "yield! of an array of struct pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! [| struct ("ONE", second) |] + }) + "yield! of key/value pairs", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! Seq.singleton (KeyValuePair ("ONE", second)) + }) + "yield! of a sorted map", + (fun first second -> ignoreCase { + yield struct ("one", first) + yield! SortedMap.singleton "ONE" second + }) + |] + + /// The expressions that take a sequence, by name, with the name of the sequence parameter and the expression on a null + /// sequence. The expression rejects the sequence when the member runs, and not when the buffer does. + static let nullSequenceExpressions : Map unit))> = + Map.ofArray [| + "yield! of a sequence of struct pairs", + struct ("pairs", + fun () -> + sortedMap { yield! Unchecked.defaultof> } + |> ignore) + "yield! of a sequence of key/value pairs", + struct ("pairs", + fun () -> + sortedMap { yield! Unchecked.defaultof>> } + |> ignore) + "for", + struct ("source", + fun () -> + sortedMap { + for item in Unchecked.defaultof> do + yield struct (item, "x") + } + |> ignore) + |] + + /// The expressions that take an array or a sorted map, by name, with the name of the parameter and the expression on a + /// null one. A null array or sorted map is not a sequence for the member, which would fail with NullReferenceException + /// when the buffer runs; the expression rejects it when the member runs. + static let nullCollectionExpressions : Map unit))> = + Map.ofArray [| + "yield! of an array of struct pairs", + struct ("pairs", + fun () -> + sortedMap { yield! Unchecked.defaultof } + |> ignore) + "yield! of a sorted map", + struct ("table", + fun () -> + sortedMap { yield! Unchecked.defaultof> } + |> ignore) + |] + + /// The expressions that take a sequence, by name, with the name of the sequence parameter and the expression on a + /// default flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails + /// with InvalidOperationException when the sequence is enumerated. The last row gives a sequence of objects, which a + /// default flat list of strings is by the covariance of seq: the element type of the sequence is not the one of the + /// flat list. The keys of the map are not the objects, because the expression needs keys with comparison. + static let defaultSequenceExpressions : Map unit))> = + let defaultStructPairs = Unchecked.defaultof> :> seq<_> + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + "yield! of a sequence of struct pairs", struct ("pairs", fun () -> sortedMap { yield! defaultStructPairs } |> ignore) + "yield! of a sequence of key/value pairs", struct ("pairs", fun () -> sortedMap { yield! defaultPairs } |> ignore) + "for", + struct ("source", + fun () -> + sortedMap { + for item in defaultKeys do + yield struct (item, "x") + } + |> ignore) + "for (default flat list of strings as objects)", + struct ("source", + fun () -> + sortedMap { + for _ in FlatListOracles.defaultStringsAsObjects do + yield struct (1, "x") + } + |> ignore) + |] + + /// The names of the members that set a binding, as the rows of the test of the values that the default comparer finds + /// equal. + static member EqualValueExpressions = equalValueExpressions.Keys + + /// The names of the expressions that take an array or a sorted map and are given a null one, as the rows of their test. + static member NullCollectionExpressions = nullCollectionExpressions.Keys + + /// The names of the expressions that take a sequence and are given a null one, as the rows of their test. + static member NullSequenceExpressions = nullSequenceExpressions.Keys + + /// The names of the expressions that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceExpressions = defaultSequenceExpressions.Keys + + [] + member _.``an expression without bindings is empty`` () = + let table : SortedMap = sortedMap { () } + Assert.IsTrue (SortedMap.isEmpty table, "sortedMap { () } must be empty") + + [] + member _.``yield sets struct pairs and key value pairs`` () = + let table = sortedMap { + yield struct (2, "two") + yield KeyValuePair (1, "one") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + SortedMap.toArray table, + "sortedMap must set the yielded bindings in the order of the keys" + ) + + [] + member _.``implicit yields set the bindings`` () = + let table = sortedMap { + struct (2, "two") + struct (1, "one") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two") |], + SortedMap.toArray table, + "sortedMap must set the implicitly yielded bindings in the order of the keys" + ) + + [] + member _.``a later binding of a key replaces an earlier one`` () = + let table = sortedMap { + yield struct (1, "one") + yield struct (1, "uno") + yield KeyValuePair (1, "eins") + } + + CollectionAssert.AreEqual ( + [| struct (1, "eins") |], + SortedMap.toArray table, + "sortedMap must keep the last binding of a key" + ) + + [] + member _.``a value that the default comparer finds equal keeps the earlier binding`` (expression : string) = + // The indexer of the builder keeps the earlier binding, its key included (ADR 0001, D14); the copies are equal in + // the default comparer of the values and tell the two values apart + let first = MapInputs.Copy "uno" + let table = equalValueExpressions[expression] first (MapInputs.Copy "uno") + + let struct (key, value) = + Assert.ContainsSingle (SortedMap.toArray table, "sortedMap must merge the bindings of the equal keys") + + Assert.AreEqual("one", key, "sortedMap must keep the earlier key") + Assert.AreSame (first, value, "sortedMap must keep the earlier value") + + [] + member _.``yield! sets the bindings of sequences, arrays, maps and other expressions`` () = + let inner = sortedMap { yield struct (7, "seven") } + let dictionary = Dictionary() + dictionary[6] <- "six" + + let table = sortedMap { + yield! inner + yield! [ struct (2, "two"); struct (1, "one") ] + yield! [| struct (3, "three") |] + yield! [ KeyValuePair (4, "four") ] + yield! SortedMap.singleton 5 "five" + yield! dictionary + } + + CollectionAssert.AreEqual ( + [| 1..7 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must set the bindings of every yield! in the order of the keys" + ) + + [] + member _.``for sets the bindings of the body for each item`` () = + let table = sortedMap { + for item in 3..-1..1 do + yield struct (item, string (item * 10)) + } + + CollectionAssert.AreEqual ( + [| struct (1, "10"); struct (2, "20"); struct (3, "30") |], + SortedMap.toArray table, + "sortedMap must run the body of for for each item" + ) + + [] + member _.``while sets the bindings while the guard holds`` () = + let mutable count = 0 + + let table = sortedMap { + while count < 3 do + count <- count + 1 + yield struct (-count, string count) + } + + CollectionAssert.AreEqual ( + [| -3; -2; -1 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must run the body of while while the guard holds" + ) + + [] + member _.``if without else and following bindings combine`` () = + let table = sortedMap { + if false then + yield struct (0, "zero") + + yield struct (1, "one") + + if true then + yield struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must combine conditional and plain bindings" + ) + + [] + member _.``try with sets the bindings of the handler after an exception`` () = + let table = sortedMap { + try + yield struct (1, "one") + failwith "fails" + with _ -> + yield struct (2, "two") + } + + CollectionAssert.AreEqual ( + [| 1; 2 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must keep the bindings before the exception and add the handler's" + ) + + [] + member _.``try finally runs the compensation`` () = + let mutable compensated = false + + let table = sortedMap { + try + yield struct (1, "one") + finally + compensated <- true + } + + Assert.IsTrue (compensated, "sortedMap must run the compensation of try finally") + + CollectionAssert.AreEqual ( + [| 1 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must keep the bindings of the body of try finally" + ) + + [] + member _.``use disposes the resource after the body`` () = + let mutable disposed = false + + let table = sortedMap { + use _resource = { + new IDisposable with + member _.Dispose () = disposed <- true + } + + yield struct (1, "one") + } + + Assert.IsTrue (disposed, "sortedMap must dispose the resource of use") + + CollectionAssert.AreEqual ( + [| 1 |], + Seq.toArray (SortedMap.keys table), + "sortedMap must keep the bindings of the body of use" + ) + + [] + member _.``sortedMap orders the keys with F# generic comparison`` () = + let table = sortedMap { + for key in [| "b"; "a"; "B"; "A"; "_" |] do + yield struct (key, key.Length) + } + + CollectionAssert.AreEqual ( + [| "A"; "B"; "_"; "a"; "b" |], + Seq.toArray (SortedMap.keys table), + "sortedMap must order the keys ordinally, as Map does" + ) + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "sortedMap must order the keys with F# generic comparison" + ) + + Assert.AreSame>( + EqualityComparer.Default, + SortedMap.valueComparer table, + "sortedMap must compare the values with the default comparer" + ) + + [] + member _.``SortedMapCE without a comparer orders the keys with F# generic comparison`` () = + let ordinal = SortedMapCE() + let table = ordinal { yield struct ("b", 1) } + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "SortedMapCE () must order the keys with F# generic comparison" + ) + + [] + member _.``SortedMapCE orders the keys with the comparer that it takes`` () = + let ignoreCase = SortedMapCE StringComparer.OrdinalIgnoreCase + + // Ordinally, "B" and "C" come before "a"; ignoring the case, "a" comes first + let table = ignoreCase { + yield struct ("B", 1) + yield struct ("a", 2) + yield struct ("C", 3) + yield struct ("A", 4) + } + + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + SortedMap.keyComparer table, + "SortedMapCE must keep the key comparer" + ) + + CollectionAssert.AreEqual ( + [| 4; 1; 3 |], + SortedMap.toArray table + |> Array.map (fun struct (_, value) -> value), + "SortedMapCE must order the keys with the comparer" + ) + + Assert.AreEqual(3, SortedMap.count table, "SortedMapCE must merge the keys that the comparer considers equal") + Assert.AreEqual(4, SortedMap.find "a" table, "SortedMapCE must keep the last binding of equal keys") + + [] + member _.``yield! and for reject a null sequence`` (name : string) = + let struct (paramName, run) = nullSequenceExpressions[name] + FlatListAssert.RejectsNull ($"sortedMap %s{name}", paramName, run) + + [] + member _.``yield! rejects a null array and a null sorted map`` (name : string) = + let struct (paramName, run) = nullCollectionExpressions[name] + FlatListAssert.RejectsNull ($"sortedMap %s{name}", paramName, run) + + [] + member _.``yield! and for reject a default flat list given as a sequence`` (name : string) = + // The member rejects it as the functions of SortedMap do, when the expression runs it and not when the buffer does + let struct (paramName, run) = defaultSequenceExpressions[name] + FlatListAssert.RejectsNull ($"sortedMap %s{name}", paramName, run) + + [] + member _.``yield! and for take a flat list that is not default as a sequence, also one of strings as objects`` () = + let structPairs = FlatList.ofArray [| struct (1, "one"); struct (2, "two") |] :> seq<_> + let pairs = FlatList.ofArray [| KeyValuePair (3, "three") |] :> seq<_> + let keys = FlatList.ofArray [| 4 |] :> seq<_> + + let table = sortedMap { + yield! structPairs + yield! pairs + + for key in keys do + yield struct (key, "four") + } + + CollectionAssert.AreEqual ( + [| struct (1, "one"); struct (2, "two"); struct (3, "three"); struct (4, "four") |], + SortedMap.toArray table, + "sortedMap must take the elements of a flat list in yield! and for" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "a"; "b" |]) + + let objectTable = sortedMap { + for item in widened do + yield struct (string item, 1) + } + + CollectionAssert.AreEqual ( + [| "a"; "b" |], + Seq.toArray (SortedMap.keys objectTable), + "sortedMap must take the elements of a flat list of strings as objects in for" + ) + + [] + member _.``SortedMapCE rejects a null key comparer`` () = + let ex = + Assert.ThrowsExactly( + Action (fun () -> + SortedMapCE(Unchecked.defaultof>) + |> ignore + ), + "SortedMapCE must reject a null key comparer" + ) + + Assert.AreEqual("keyComparer", ex.ParamName, "SortedMapCE must name the key comparer") diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapContractTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapContractTests.fs new file mode 100644 index 00000000..ee81d7c8 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapContractTests.fs @@ -0,0 +1,29 @@ +namespace FSharp.Collections.Immutable.Tests + +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The reading contract for `SortedMap`. +[] +type SortedMapReadingTests () = + inherit MapContract> (SortedMapOps.reading) + +/// The transformation contract for `SortedMap`. +[] +type SortedMapTransformTests () = + inherit MapTransformContract> (SortedMapOps.transform) + +/// The mutation contract for `SortedMap`. +[] +type SortedMapMutationTests () = + inherit MapMutationContract> (SortedMapOps.mutation) + +/// The creation contract for `SortedMap` and its extension members, with the default comparers. +[] +type SortedMapCreationTests () = + inherit MapCreationContract> (SortedMapOps.creation) + +/// The creation contract for the members of `CustomComparersSortedMap`, with a key comparer. +[] +type CustomComparersSortedMapCreationTests () = + inherit MapCreationContract> (SortedMapOps.customComparersCreation) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapExtensionsTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapExtensionsTests.fs new file mode 100644 index 00000000..21589653 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapExtensionsTests.fs @@ -0,0 +1,261 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Reflection +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// The static members of `SortedMapExtensions`, which `SortedMap.ofArray`, `SortedMap.ofList` and `SortedMap.ofSeq` reach +/// through the abbreviation, called as user code calls them (ADR 0001, D13). +[] +type SortedMapExtensionsTests () = + + /// The creations of a sorted map from bindings that repeat the key "a", by the member and the shape that they call. + static let repeatedKeyCreations : Map SortedMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun () -> SortedMap.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |]) + "ofArray of reference tuples", (fun () -> SortedMap.ofArray [| "a", 1; "b", 2; "a", 3 |]) + "ofArray of struct tuples", (fun () -> SortedMap.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |]) + "ofList of key/value pairs", + (fun () -> SortedMap.ofList [ KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) ]) + "ofList of reference tuples", (fun () -> SortedMap.ofList [ "a", 1; "b", 2; "a", 3 ]) + "ofList of struct tuples", (fun () -> SortedMap.ofList [ struct ("a", 1); struct ("b", 2); struct ("a", 3) ]) + "ofSeq of key/value pairs", + (fun () -> SortedMap.ofSeq (Seq.ofArray [| KeyValuePair ("a", 1); KeyValuePair ("b", 2); KeyValuePair ("a", 3) |])) + "ofSeq of reference tuples", (fun () -> SortedMap.ofSeq (Seq.ofArray [| "a", 1; "b", 2; "a", 3 |])) + "ofSeq of struct tuples", + (fun () -> SortedMap.ofSeq (Seq.ofArray [| struct ("a", 1); struct ("b", 2); struct ("a", 3) |])) + |] + + /// The creations of a sorted map that binds each word to its length, by the member and the shape that they call. + static let wordCreations : Map SortedMap> = + Map.ofArray [| + "ofArray of key/value pairs", + (fun words -> + SortedMap.ofArray ( + words + |> Array.map (fun word -> KeyValuePair (word, word.Length)) + ) + ) + "ofArray of reference tuples", (fun words -> SortedMap.ofArray (words |> Array.map (fun word -> word, word.Length))) + "ofArray of struct tuples", + (fun words -> SortedMap.ofArray (words |> Array.map (fun word -> struct (word, word.Length)))) + // The members under test take lists + "ofList of key/value pairs", (fun words -> SortedMap.ofList [ for word in words -> KeyValuePair (word, word.Length) ]) + "ofList of reference tuples", (fun words -> SortedMap.ofList [ for word in words -> word, word.Length ]) + "ofList of struct tuples", (fun words -> SortedMap.ofList [ for word in words -> struct (word, word.Length) ]) + "ofSeq of key/value pairs", + (fun words -> + SortedMap.ofSeq ( + words + |> Seq.map (fun word -> KeyValuePair (word, word.Length)) + ) + ) + "ofSeq of reference tuples", (fun words -> SortedMap.ofSeq (words |> Seq.map (fun word -> word, word.Length))) + "ofSeq of struct tuples", (fun words -> SortedMap.ofSeq (words |> Seq.map (fun word -> struct (word, word.Length)))) + |] + + /// The creations of a sorted map of "one" and "two" in a pipeline, by the shape and the source of the bindings; every + /// source has "two" first. + static let pipelines : Map SortedMap> = + Map.ofArray [| + "an array of key/value pairs", + (fun () -> + [| KeyValuePair ("two", 2); KeyValuePair ("one", 1) |] + |> SortedMap.ofArray + ) + "an array of reference tuples", (fun () -> [| "two", 2; "one", 1 |] |> SortedMap.ofArray) + "an array of struct tuples", + (fun () -> + [| struct ("two", 2); struct ("one", 1) |] + |> SortedMap.ofArray + ) + "a list of key/value pairs", + (fun () -> + [ KeyValuePair ("two", 2); KeyValuePair ("one", 1) ] + |> SortedMap.ofList + ) + "a list of reference tuples", (fun () -> [ "two", 2; "one", 1 ] |> SortedMap.ofList) + "a list of struct tuples", (fun () -> [ struct ("two", 2); struct ("one", 1) ] |> SortedMap.ofList) + "a sequence of key/value pairs", + (fun () -> + Seq.ofArray [| KeyValuePair ("two", 2); KeyValuePair ("one", 1) |] + |> SortedMap.ofSeq + ) + "a sequence of reference tuples", (fun () -> Seq.ofArray [| "two", 2; "one", 1 |] |> SortedMap.ofSeq) + "a sequence of struct tuples", + (fun () -> + Seq.ofArray [| struct ("two", 2); struct ("one", 1) |] + |> SortedMap.ofSeq + ) + "a dictionary", (fun () -> dict [| "two", 2; "one", 1 |] |> SortedMap.ofSeq) + "an F# map", (fun () -> Map.ofArray [| "two", 2; "one", 1 |] |> SortedMap.ofSeq) + "a hash map", (fun () -> HashMap.ofArray [| "two", 2; "one", 1 |] |> SortedMap.ofSeq) + "a sorted map", + (fun () -> + SortedMap.ofArray [| "two", 2; "one", 1 |] + |> SortedMap.ofSeq + ) + |] + + /// The creations of a sorted map from the given bindings, by the member and the shape that they call. + static let shapedCreations : Map SortedMap> = + Map.ofArray [| + "ofArray of key/value pairs", (fun bindings -> SortedMap.ofArray (Array.map MapInputs.ToKeyValuePair bindings)) + "ofArray of reference tuples", (fun bindings -> SortedMap.ofArray (Array.map MapInputs.ToTuple bindings)) + "ofArray of struct tuples", (fun bindings -> SortedMap.ofArray bindings) + "ofList of key/value pairs", + (fun bindings -> + SortedMap.ofList ( + bindings + |> Array.map MapInputs.ToKeyValuePair + |> List.ofArray + ) + ) + "ofList of reference tuples", + (fun bindings -> SortedMap.ofList (bindings |> Array.map MapInputs.ToTuple |> List.ofArray)) + "ofList of struct tuples", (fun bindings -> SortedMap.ofList (List.ofArray bindings)) + "ofSeq of key/value pairs", (fun bindings -> SortedMap.ofSeq (Seq.map MapInputs.ToKeyValuePair bindings)) + "ofSeq of reference tuples", (fun bindings -> SortedMap.ofSeq (Seq.map MapInputs.ToTuple bindings)) + "ofSeq of struct tuples", (fun bindings -> SortedMap.ofSeq (Seq.ofArray bindings)) + |] + + /// The names of the creations from bindings that repeat a key, as the rows of the test of the last binding. + static member RepeatedKeyCreations = repeatedKeyCreations.Keys + + /// The names of the creations from words, as the rows of the test of the order of strings. + static member WordCreations = wordCreations.Keys + + /// The names of the sources of the pipelines: the rows of the test of the pipelines. + static member Pipelines = pipelines.Keys + + /// The names of the creations from the given bindings, as the rows of the test of the values that the default comparer + /// finds equal. + static member ShapedCreations = shapedCreations.Keys + + /// The keys of the sorted maps that the test of an existing map gives to ofSeq. The property builds new arrays on each + /// read. + static member Keys : int[] seq = seq { + [||] + [| 42 |] + [| 5; -3; 8; 0; 12 |] + } + + [] + member _.``every shape keeps the last binding of a key as Map does`` (creation : string) = + CollectionAssert.AreEqual ( + Map.toArray (Map.ofArray [| "a", 1; "b", 2; "a", 3 |]) + |> Array.map (fun (key, value) -> struct (key, value)), + SortedMap.toArray (repeatedKeyCreations[creation]()), + "The sorted map must keep the last binding of a key in the order of the keys, as Map does" + ) + + [] + member _.``every shape keeps the earlier binding of a key for a value that the default comparer finds equal`` + (creation : string) + = + // The indexer of the builder keeps the earlier binding, its key included (ADR 0001, D14); the copies are equal in + // the default comparers and tell the two bindings apart + let firstKey = MapInputs.Copy "one" + let firstValue = MapInputs.Copy "uno" + + let table = + shapedCreations[creation] [| struct (firstKey, firstValue); struct (MapInputs.Copy "one", MapInputs.Copy "uno") |] + + let struct (key, value) = + Assert.ContainsSingle (SortedMap.toArray table, "The sorted map must merge the bindings of the equal keys") + + Assert.AreSame (firstKey, key, "The sorted map must keep the earlier key") + Assert.AreSame (firstValue, value, "The sorted map must keep the earlier value") + + [] + member _.``every shape orders strings ordinally as Map does`` (creation : string) = + // Ordinally, the digits, the upper case letters and the underscore come before the lower case letters, and "10" + // before "9"; the order of a culture differs + let words = [| "b"; "a"; "B"; "A"; "_"; "10"; "9" |] + let table = wordCreations[creation] words + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "The sorted map must order its keys with F# generic comparison" + ) + + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (Map.ofArray (words |> Array.map (fun word -> word, word.Length)))), + Seq.toArray (SortedMap.keys table), + "The sorted map must order strings ordinally, as Map does" + ) + + [] + member _.``the creation members resolve the shape of the bindings in a pipeline`` (source : string) = + CollectionAssert.AreEqual ( + [| struct ("one", 1); struct ("two", 2) |], + SortedMap.toArray (pipelines[source]()), + "The pipeline must create the bindings of its source in the order of the keys" + ) + + [] + member _.``ofSeq returns a sorted map with the default comparers itself`` (keys : int array) = + let table = SortedMap.ofArray (keys |> Array.map (fun key -> struct (key, string key))) + Assert.AreSame ( + table, + SortedMap.ofSeq table, + "SortedMap.ofSeq must return a sorted map with the default comparers itself" + ) + + [] + member _.``ofSeq gives a sorted map with other comparers the default comparers`` () = + let ignoreCase = + (SortedMap.forKeyComparer StringComparer.OrdinalIgnoreCase).ofArray [| "one", 1; "Two", 2 |] + + let table = SortedMap.ofSeq ignoreCase + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "SortedMap.ofSeq must give a sorted map the key comparer of F# generic comparison" + ) + + // Ordinally, "Two" comes before "one"; ignoring the case, after it + CollectionAssert.AreEqual ( + [| struct ("Two", 2); struct ("one", 1) |], + SortedMap.toArray table, + "SortedMap.ofSeq must keep the bindings and order them ordinally" + ) + + Assert.IsFalse (SortedMap.containsKey "two" table, "SortedMap.ofSeq must look the keys up with F# generic comparison") + + [] + member _.``the creation members are public static methods of SortedMapExtensions that the module does not have`` + (name : string) + = + // C# sees the members only as these methods, and a reflection over the module lists none of them + let assembly = typeof>.Assembly + + let typeNamed (typeName : string) = + match assembly.GetType typeName with + | null -> failwith $"The library must have the type %s{typeName}" + | found -> found + + let methodsNamed (typeName : string) = + (typeNamed typeName) + .GetMethods( + BindingFlags.Public + ||| BindingFlags.Static + ||| BindingFlags.DeclaredOnly + ) + |> Array.filter (fun candidate -> String.Equals (candidate.Name, name, StringComparison.Ordinal)) + + Assert.HasCount ( + 3, + methodsNamed "FSharp.Collections.Immutable.SortedMapExtensions", + $"SortedMapExtensions must have one overload of %s{name} for each shape of the bindings" + ) + + Assert.IsEmpty ( + methodsNamed "FSharp.Collections.Immutable.SortedMap", + $"The SortedMap module must have no function %s{name}, so that SortedMap.ofSeq reaches the extension members" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOps.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOps.fs new file mode 100644 index 00000000..ee318736 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOps.fs @@ -0,0 +1,141 @@ +/// The functions of the `SortedMap` module for the contract tests. +[] +module FSharp.Collections.Immutable.Tests.SortedMapOps + +open FSharp.Collections.Immutable + +/// A comparer of keys that orders them by their absolute values, for the comparer struct. +let absoluteKeyOrder = + ComparisonIdentity.FromFunction (fun (left : int) (right : int) -> compare (abs left) (abs right)) + +/// The functions of the reading facet. `SortedMap` names its map parameter `table`, as the `Map` module does. +let reading : MapOps> = { + Name = nameof SortedMap + ParamName = id + OfSeq = SortedMap.ofSeq + ToArray = SortedMap.toArray + + Count = SortedMap.count + IsEmpty = SortedMap.isEmpty + Keys = fun table -> SortedMap.keys table + Values = fun table -> SortedMap.values table + + Find = SortedMap.find + TryFind = SortedMap.tryFind + + ContainsBinding = SortedMap.containsBinding + ContainsKey = SortedMap.containsKey + Exists = SortedMap.exists + FindKey = SortedMap.findKey + Forall = SortedMap.forall + Pick = SortedMap.pick + TryFindKey = SortedMap.tryFindKey + TryPick = SortedMap.tryPick + + Fold = SortedMap.fold + FoldBack = SortedMap.foldBack + Iter = SortedMap.iter + + ToList = SortedMap.toList + ToSeq = SortedMap.toSeq +} + +/// The functions of the transformation facet. +let transform : MapTransformOps> = { + Name = nameof SortedMap + ParamName = id + OfSeq = SortedMap.ofSeq + ToArray = SortedMap.toArray + + Choose = fun chooser table -> SortedMap.choose chooser table |> SortedMap.toArray + Filter = SortedMap.filter + Map = fun mapping table -> SortedMap.map mapping table |> SortedMap.toArray + Partition = SortedMap.partition +} + +/// The functions of the mutation facet: `addRange` and `setItems` of the module over key/value pairs, and of its nested +/// modules over tuples (ADR 0001, D13). +let mutation : MapMutationOps> = { + Name = nameof SortedMap + ParamName = id + OfSeq = SortedMap.ofSeq + ToArray = SortedMap.toArray + + Add = SortedMap.add + Change = SortedMap.change + Clear = SortedMap.clear + Remove = SortedMap.remove + RemoveRange = SortedMap.removeRange + SetItem = SortedMap.setItem + + KeyValuePairs = { AddRange = SortedMap.addRange; SetItems = SortedMap.setItems } + Tuples = { AddRange = SortedMap.Tuple.addRange; SetItems = SortedMap.Tuple.setItems } + StructTuples = { + AddRange = SortedMap.ValueTuple.addRange + SetItems = SortedMap.ValueTuple.setItems + } +} + +/// The functions of the creation facet with the default comparers: the functions of the module and the overloads of the +/// extension members, one shape per field (ADR 0001, D13). +let creation : MapCreationOps> = { + Name = nameof SortedMap + ParamName = id + OfSeq = SortedMap.ofSeq + ToArray = SortedMap.toArray + ContainsKey = SortedMap.containsKey + EquivalentKey = id + + Empty = fun () -> SortedMap.empty + KeyValuePairs = { + OfArray = SortedMap.ofArray + OfList = SortedMap.ofList + OfSeq = SortedMap.ofSeq + } + Tuples = { + OfArray = SortedMap.ofArray + OfList = SortedMap.ofList + OfSeq = SortedMap.ofSeq + } + StructTuples = { + OfArray = SortedMap.ofArray + OfList = SortedMap.ofList + OfSeq = SortedMap.ofSeq + } + OfSeqBy = SortedMap.ofSeqBy + Singleton = SortedMap.singleton +} + +/// The comparer struct whose key comparer orders the keys by their absolute values. +let private absoluteKeys : CustomComparersSortedMap = + SortedMap.forKeyComparer absoluteKeyOrder + +/// The functions of the creation facet through the comparer struct with a key comparer of absolute values (ADR 0001, D12): +/// every created map finds a key by its negation. +let customComparersCreation : MapCreationOps> = { + Name = nameof CustomComparersSortedMap + ParamName = id + OfSeq = SortedMap.ofSeq + ToArray = SortedMap.toArray + ContainsKey = SortedMap.containsKey + EquivalentKey = fun key -> -key + + Empty = fun () -> absoluteKeys.empty + KeyValuePairs = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + Tuples = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + StructTuples = { + OfArray = absoluteKeys.ofArray + OfList = absoluteKeys.ofList + OfSeq = absoluteKeys.ofSeq + } + OfSeqBy = absoluteKeys.ofSeqBy + Singleton = absoluteKeys.singleton +} diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOptionTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOptionTests.fs new file mode 100644 index 00000000..383a54fc --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapOptionTests.fs @@ -0,0 +1,173 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open FSharp.Collections.Immutable +open FSharp.Collections.Immutable.Tuple +open FSharp.Collections.Immutable.Option +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The predicates over the keys of the bindings of tryFindKey in the tests of the Option namespace, as the +/// cases of their rows. +/// +/// +/// MSTest names a row after its values, and a function has no readable name, so a row holds a case and the test takes the +/// function of the case. +/// +[] +type KeyPredicate = + /// The bindings of keys above ten. + | AboveTen + /// The bindings of negative keys. + | Negative + /// No binding. + | Never + + /// Every predicate: the rows of the test of tryFindKey. The property builds its rows on each read. + static member All : KeyPredicate seq = seq { + KeyPredicate.AboveTen + KeyPredicate.Negative + KeyPredicate.Never + } + + /// Tells whether a binding satisfies the predicate. + member this.Matches (key : int) (_ : string) = + match this with + | KeyPredicate.AboveTen -> key > 10 + | KeyPredicate.Negative -> key < 0 + | KeyPredicate.Never -> false + +// 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 `SortedMap` module of the `Option` namespace. +[] +type SortedMapOptionTests () = + + static let table = FSharp.Collections.Immutable.SortedMap.ofSeq MapInputs.Several + + static let oracle = MapInputs.ToMap MapInputs.Several + + /// A chooser that returns a value for the bindings of negative keys only. + static let negatedIfNegative (key : int) (value : string) = if key < 0 then Some (value.Length - key) else None + + [] + member _.``tryFind returns an option`` (key : int) = + let found : string option = SortedMap.tryFind key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.SortedMap.tryFind key table, + found, + "Option.SortedMap.tryFind must match the default" + ) + + [] + member _.``tryGetKey returns an option`` (key : int) = + let found : int option = SortedMap.tryGetKey key table + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.SortedMap.tryGetKey key table, + found, + "Option.SortedMap.tryGetKey must match the default" + ) + + [] + member _.``pick takes a chooser of options as Map.pick does`` () = + let picked : int = SortedMap.pick negatedIfNegative table + + Assert.AreEqual( + Map.pick negatedIfNegative oracle, + picked, + "Option.SortedMap.pick must pick the first value in the order of the keys, as Map.pick does" + ) + + [)>] + member _.``tryFindKey returns an option as Map.tryFindKey does`` (predicate : KeyPredicate) = + let found : int option = SortedMap.tryFindKey predicate.Matches table + + Assert.AreEqual( + Map.tryFindKey predicate.Matches oracle, + found, + "Option.SortedMap.tryFindKey must find the first key in the order of the keys, as Map.tryFindKey does" + ) + + OverrideAssert.OptionMatches ( + FSharp.Collections.Immutable.SortedMap.tryFindKey predicate.Matches table, + found, + "Option.SortedMap.tryFindKey must match the default" + ) + + [] + member _.``tryPick takes a chooser of options and returns an option as Map.tryPick does`` () = + let found : int option = SortedMap.tryPick negatedIfNegative table + + Assert.AreEqual( + Map.tryPick negatedIfNegative oracle, + found, + "Option.SortedMap.tryPick must pick the first value in the order of the keys, as Map.tryPick does" + ) + + let nothing : int option = SortedMap.tryPick (fun _ _ -> None) table + Assert.AreEqual(None, nothing, "Option.SortedMap.tryPick must return None when the chooser returns nothing") + + [] + member _.``change takes a function of options as Map.change does`` (key : int) = + let extend (current : string option) = current |> Option.map (fun value -> value + "!") + let changed : SortedMap = SortedMap.change key extend table + + CollectionAssert.AreEqual ( + MapInputs.OfMap (Map.change key extend oracle), + FSharp.Collections.Immutable.SortedMap.toArray changed, + "Option.SortedMap.change must change as Map.change does" + ) + + [] + member _.``change leaves the binding as it is for a value that the value comparer finds equal`` () = + // The function calls SortedMap.change, which keeps the binding, its key included, for an equal value (ADR 0001, D14) + let comparers = + FSharp.Collections.Immutable.SortedMap.forComparers StringComparer.OrdinalIgnoreCase StringComparer.OrdinalIgnoreCase + + let ignoreCase = comparers.ofArray [| "one", "uno" |] + let changed : SortedMap = SortedMap.change "ONE" (fun _ -> Some "UNO") ignoreCase + + Assert.AreEqual( + Some "uno", + SortedMap.tryFind "one" changed, + "Option.SortedMap.change must keep the current value" + ) + + Assert.AreEqual( + Some "one", + SortedMap.tryGetKey "ONE" changed, + "Option.SortedMap.change must keep the current key" + ) + + Assert.AreSame (ignoreCase, changed, "Option.SortedMap.change must return the input itself") + + [] + member _.``choose takes a chooser of options`` () = + let chosen : SortedMap = SortedMap.choose negatedIfNegative table + + CollectionAssert.AreEqual ( + oracle + |> Map.toArray + |> Array.choose (fun (key, value) -> + negatedIfNegative key value + |> Option.map (fun chosen -> struct (key, chosen)) + ), + FSharp.Collections.Immutable.SortedMap.toArray chosen, + "Option.SortedMap.choose must keep the chosen values in the order of the keys" + ) + + [] + member _.``the functions without an option keep resolving to the other modules`` () = + let mapped : SortedMap = SortedMap.map (fun _ (value : string) -> value.Length) table + let pairs : (int * string) list = SortedMap.toList table + + Assert.AreEqual( + MapInputs.Several.Length, + FSharp.Collections.Immutable.SortedMap.count mapped, + "SortedMap.map must resolve to the default module" + ) + + CollectionAssert.AreEqual (Map.toArray oracle, List.toArray pairs, "SortedMap.toList must resolve to the Tuple module") diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTests.fs new file mode 100644 index 00000000..864a9fc8 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTests.fs @@ -0,0 +1,1262 @@ +namespace FSharp.Collections.Immutable.Tests + +open System +open System.Collections.Generic +open System.Collections.Immutable +open FSharp.Collections.Immutable +open Microsoft.VisualStudio.TestTools.UnitTesting + +/// +/// The sets of keys of the tests of the order of a sorted map, as the cases of their rows. +/// +/// +/// MSTest names a row after its values, and a set of bindings is too long to read in a name, so a row holds a case and the +/// test takes the bindings of the case. The checks are generic in the type of the keys, which differs from case to case. +/// +[] +type OrderedKeys = + /// String keys whose ordinal order, the order of F# generic comparison, is not the order of a culture: the upper case + /// letters and the underscore come before the lower case letters, and the digits compare one character at a time. + | Strings + /// Negative keys, zero and positive keys, in no order. + | Negatives + /// Tuple keys, which F# compares item by item: the numbers first, then the strings ordinally. + | Tuples + /// One key. + | OneKey + + /// The sets of several keys: the rows of the tests of the order. The property builds its rows on each read. + static member Several : OrderedKeys seq = seq { + OrderedKeys.Strings + OrderedKeys.Negatives + OrderedKeys.Tuples + } + + /// Every set: the rows of the tests of the smallest and the largest binding. + static member All : OrderedKeys seq = seq { + yield! OrderedKeys.Several + OrderedKeys.OneKey + } + +/// +/// The functions and expressions that create a sorted map from bindings with F# generic comparison of the keys, as the +/// cases of the rows of the tests of the order. +/// +[] +type OrderedCreation = + /// A builder that the bindings fill one by one. + | Builder + /// add of each binding to the empty sorted map. + | Empty + /// ofArray of struct tuples. + | OfArrayOfStructTuples + /// ofList of struct tuples. + | OfListOfStructTuples + /// ofSeq of key/value pairs. + | OfSeqOfKeyValuePairs + /// ofSeq of struct tuples. + | OfSeqOfStructTuples + /// add of each binding to the sorted map of the last one. + | Singleton + /// The sortedMap { } expression. + | Expression + + /// Every creation: the rows of the tests of the order. The property builds its rows on each read. + static member All : OrderedCreation seq = seq { + OrderedCreation.Builder + OrderedCreation.Empty + OrderedCreation.OfArrayOfStructTuples + OrderedCreation.OfListOfStructTuples + OrderedCreation.OfSeqOfKeyValuePairs + OrderedCreation.OfSeqOfStructTuples + OrderedCreation.Singleton + OrderedCreation.Expression + } + + /// Creates a sorted map of the bindings, whose keys are distinct: add rejects another value of a key, as the BCL does. + member this.Create<'Key, 'Value when 'Key : not null and 'Key : comparison> (bindings : struct ('Key * 'Value) array) = + let addAll (table : SortedMap<'Key, 'Value>) = + (table, bindings) + ||> Array.fold (fun table struct (key, value) -> SortedMap.add key value table) + + match this with + | OrderedCreation.Builder -> + let builder = SortedMap.builder () + + for struct (key, value) in bindings do + builder[key] <- value + + SortedMap.ofBuilder builder + | OrderedCreation.Empty -> addAll SortedMap.empty + | OrderedCreation.OfArrayOfStructTuples -> SortedMap.ofArray bindings + // The member under test takes a list + | OrderedCreation.OfListOfStructTuples -> SortedMap.ofList (List.ofArray bindings) + | OrderedCreation.OfSeqOfKeyValuePairs -> + SortedMap.ofSeq ( + bindings + |> Array.map (fun struct (key, value) -> KeyValuePair (key, value)) + ) + | OrderedCreation.OfSeqOfStructTuples -> SortedMap.ofSeq bindings + | OrderedCreation.Singleton -> + let struct (lastKey, lastValue) = Array.last bindings + addAll (SortedMap.singleton lastKey lastValue) + | OrderedCreation.Expression -> sortedMap { yield! bindings } + +/// The behaviour of `SortedMap` that the shared contracts do not cover: the order of the keys, which is the order of the +/// FSharp.Core `Map`, the comparers, `minKeyValue`, `maxKeyValue` and `singleton`, null inputs, shared results and the +/// semantics of the functions that wrap the BCL. +[] +type SortedMapTests () = + + static let nullTable = Unchecked.defaultof> + + static let stringBindings = [| + struct ("b", 1) + struct ("a", 2) + struct ("B", 3) + struct ("A", 4) + struct ("10", 5) + struct ("9", 6) + struct ("_", 7) + |] + + static let negativeBindings = [| + struct (5, "five") + struct (-3, "minus three") + struct (0, "zero") + struct (-12, "minus twelve") + struct (8, "eight") + |] + + static let tupleBindings = [| + struct ((1, "b"), 1) + struct ((1, "B"), 2) + struct ((0, "z"), 3) + struct ((-1, "a"), 4) + struct ((1, "a"), 5) + |] + + static let oneBinding = [| struct (42, "answer") |] + + /// A comparer that orders integers from the largest to the smallest. + static let descending = ComparisonIdentity.FromFunction (fun (left : int) (right : int) -> compare right left) + + /// A value comparer that differs from the default one: it compares strings by their length. + static let byLength = + HashIdentity.FromFunctions (fun (value : string) -> value.Length) (fun left right -> left.Length = right.Length) + + /// A map whose keys ignore the case and whose values compare by their length. + static let customTable () = + (SortedMap.forComparers StringComparer.OrdinalIgnoreCase byLength).ofArray [| + "one", "uno" + "Two", "dos" + "three", "tres" + |] + + /// A map with the default comparers. + static let defaultTable () = SortedMap.ofArray [| struct ("one", "uno"); struct ("two", "dos") |] + + /// The FSharp.Core map of bindings, the oracle of the order. + static let toMap (bindings : struct ('Key * 'Value) array) = + bindings + |> Array.map (fun struct (key, value) -> key, value) + |> Map.ofArray + + /// The bindings of an FSharp.Core map, in its order. + static let ofMap (map : Map<'Key, 'Value>) = + map + |> Map.toArray + |> Array.map (fun (key, value) -> struct (key, value)) + + /// Asserts that a sorted map has the bindings of an FSharp.Core map, in the order of that map. + static let inMapOrder (expected : Map<'Key, 'Value>) (table : SortedMap<'Key, 'Value>) (message : string) = + CollectionAssert.AreEqual (ofMap expected, SortedMap.toArray table, message) + + /// Asserts that the functions that visit the bindings one by one follow the order of the FSharp.Core map of them. + static let assertVisitingOrder (bindings : struct ('Key * 'Value) array) = + let expected = toMap bindings + let table = SortedMap.ofArray bindings + let message (name : string) = $"SortedMap.%s{name} must follow the order of Map" + let record (state : struct ('Key * 'Value) array) key value = Array.append state [| struct (key, value) |] + let recordBack key value (state : struct ('Key * 'Value) array) = Array.append [| struct (key, value) |] state + let visited = ResizeArray() + SortedMap.iter (fun key value -> visited.Add (struct (key, value))) table + let tested = ResizeArray<'Key>() + + SortedMap.exists + (fun key _ -> + tested.Add key + false + ) + table + |> ignore + + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys expected), + Seq.toArray (SortedMap.keys table), + message (nameof SortedMap.keys) + ) + + CollectionAssert.AreEqual ( + Seq.toArray (Map.values expected), + Seq.toArray (SortedMap.values table), + message (nameof SortedMap.values) + ) + + CollectionAssert.AreEqual (ofMap expected, SortedMap.toArray table, message (nameof SortedMap.toArray)) + CollectionAssert.AreEqual (ofMap expected, Seq.toArray (SortedMap.toSeq table), message (nameof SortedMap.toSeq)) + + CollectionAssert.AreEqual ( + Map.fold record [||] expected, + SortedMap.fold record [||] table, + message (nameof SortedMap.fold) + ) + + CollectionAssert.AreEqual ( + Map.foldBack recordBack expected [||], + SortedMap.foldBack recordBack table [||], + message (nameof SortedMap.foldBack) + ) + + CollectionAssert.AreEqual (ofMap expected, visited.ToArray (), message (nameof SortedMap.iter)) + CollectionAssert.AreEqual (Seq.toArray (Map.keys expected), tested.ToArray (), message (nameof SortedMap.exists)) + + Assert.AreEqual<'Key>( + Map.findKey (fun _ _ -> true) expected, + SortedMap.findKey (fun _ _ -> true) table, + message (nameof SortedMap.findKey) + ) + + Assert.AreEqual<'Key>( + Map.pick (fun key _ -> Some key) expected, + SortedMap.pick (fun key _ -> ValueSome key) table, + message (nameof SortedMap.pick) + ) + + /// Asserts that the transformations that rebuild a sorted map keep the order of the FSharp.Core map of its bindings. + static let assertTransformationOrder (bindings : struct ('Key * 'Value) array) = + let expected = toMap bindings + let table = SortedMap.ofArray bindings + + // Every second key in the order of Map, so that the predicate keeps keys from the whole range + let kept = + Map.keys expected + |> Seq.indexed + |> Seq.filter (fun (index, _) -> index % 2 = 0) + |> Seq.map snd + |> Set.ofSeq + + let predicate key (_ : 'Value) = kept.Contains key + let mapping (key : 'Key) (value : 'Value) = struct (key, value) + + let chooser key value = + if predicate key value then + ValueSome (mapping key value) + else + ValueNone + + let message (name : string) = $"SortedMap.%s{name} must keep the order of Map" + let struct (accepted, rejected) = SortedMap.partition predicate table + let expectedAccepted, expectedRejected = Map.partition predicate expected + + inMapOrder (Map.filter predicate expected) (SortedMap.filter predicate table) (message (nameof SortedMap.filter)) + inMapOrder expectedAccepted accepted (message (nameof SortedMap.partition)) + inMapOrder expectedRejected rejected (message (nameof SortedMap.partition)) + inMapOrder (Map.map mapping expected) (SortedMap.map mapping table) (message (nameof SortedMap.map)) + + inMapOrder + (Map.filter predicate expected |> Map.map mapping) + (SortedMap.choose chooser table) + (message (nameof SortedMap.choose)) + + /// Asserts that minKeyValue returns the binding of the smallest key as Map does. + static let assertMinimum (bindings : struct ('Key * 'Value) array) = + let struct (minKey, minValue) = SortedMap.minKeyValue (SortedMap.ofArray bindings) + + Assert.AreEqual<'Key * 'Value>( + Map.minKeyValue (toMap bindings), + (minKey, minValue), + "SortedMap.minKeyValue must return the binding of the smallest key" + ) + + /// Asserts that maxKeyValue returns the binding of the largest key as Map does. + static let assertMaximum (bindings : struct ('Key * 'Value) array) = + let struct (maxKey, maxValue) = SortedMap.maxKeyValue (SortedMap.ofArray bindings) + + Assert.AreEqual<'Key * 'Value>( + Map.maxKeyValue (toMap bindings), + (maxKey, maxValue), + "SortedMap.maxKeyValue must return the binding of the largest key" + ) + + /// The functions that take a map, by name, on a null map. The rows of the tests are the names: a function has no + /// readable name in a row. + static let nullTableCalls : Map unit> = + Map.ofArray [| + nameof SortedMap.add, (fun () -> SortedMap.add "a" "b" nullTable |> ignore) + nameof SortedMap.addRange, + (fun () -> + SortedMap.addRange [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof SortedMap.change, (fun () -> SortedMap.change "a" id nullTable |> ignore) + nameof SortedMap.choose, + (fun () -> + SortedMap.choose (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof SortedMap.clear, (fun () -> SortedMap.clear nullTable |> ignore) + nameof SortedMap.containsBinding, (fun () -> SortedMap.containsBinding "a" "b" nullTable |> ignore) + nameof SortedMap.containsKey, (fun () -> SortedMap.containsKey "a" nullTable |> ignore) + nameof SortedMap.containsValue, (fun () -> SortedMap.containsValue "a" nullTable |> ignore) + nameof SortedMap.count, (fun () -> SortedMap.count nullTable |> ignore) + nameof SortedMap.exists, (fun () -> SortedMap.exists (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.filter, (fun () -> SortedMap.filter (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.find, (fun () -> SortedMap.find "a" nullTable |> ignore) + nameof SortedMap.findKey, (fun () -> SortedMap.findKey (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.fold, + (fun () -> + SortedMap.fold (fun state _ _ -> state) 0 nullTable + |> ignore + ) + nameof SortedMap.foldBack, + (fun () -> + SortedMap.foldBack (fun _ _ state -> state) nullTable 0 + |> ignore + ) + nameof SortedMap.forall, (fun () -> SortedMap.forall (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.isEmpty, (fun () -> SortedMap.isEmpty nullTable |> ignore) + nameof SortedMap.iter, (fun () -> SortedMap.iter (fun _ _ -> ()) nullTable) + nameof SortedMap.keyComparer, (fun () -> SortedMap.keyComparer nullTable |> ignore) + nameof SortedMap.keys, (fun () -> SortedMap.keys nullTable |> ignore) + nameof SortedMap.map, (fun () -> SortedMap.map (fun _ value -> value) nullTable |> ignore) + nameof SortedMap.maxKeyValue, (fun () -> SortedMap.maxKeyValue nullTable |> ignore) + nameof SortedMap.minKeyValue, (fun () -> SortedMap.minKeyValue nullTable |> ignore) + nameof SortedMap.partition, (fun () -> SortedMap.partition (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.pick, + (fun () -> + SortedMap.pick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof SortedMap.remove, (fun () -> SortedMap.remove "a" nullTable |> ignore) + nameof SortedMap.removeRange, (fun () -> SortedMap.removeRange [| "a" |] nullTable |> ignore) + nameof SortedMap.setItem, (fun () -> SortedMap.setItem "a" "b" nullTable |> ignore) + nameof SortedMap.setItems, + (fun () -> + SortedMap.setItems [| KeyValuePair ("a", "b") |] nullTable + |> ignore + ) + nameof SortedMap.toArray, (fun () -> SortedMap.toArray nullTable |> ignore) + nameof SortedMap.toBuilder, (fun () -> SortedMap.toBuilder nullTable |> ignore) + nameof SortedMap.toList, (fun () -> SortedMap.toList nullTable |> ignore) + nameof SortedMap.toSeq, (fun () -> SortedMap.toSeq nullTable |> ignore) + nameof SortedMap.tryFind, (fun () -> SortedMap.tryFind "a" nullTable |> ignore) + nameof SortedMap.tryFindKey, (fun () -> SortedMap.tryFindKey (fun _ _ -> true) nullTable |> ignore) + nameof SortedMap.tryGetKey, (fun () -> SortedMap.tryGetKey "a" nullTable |> ignore) + nameof SortedMap.tryPick, + (fun () -> + SortedMap.tryPick (fun _ value -> ValueSome value) nullTable + |> ignore + ) + nameof SortedMap.update, (fun () -> SortedMap.update ignore nullTable |> ignore) + nameof SortedMap.valueComparer, (fun () -> SortedMap.valueComparer nullTable |> ignore) + nameof SortedMap.values, (fun () -> SortedMap.values nullTable |> ignore) + "Tuple.addRange", (fun () -> SortedMap.Tuple.addRange [| "a", "b" |] nullTable |> ignore) + "Tuple.setItems", (fun () -> SortedMap.Tuple.setItems [| "a", "b" |] nullTable |> ignore) + "ValueTuple.addRange", + (fun () -> + SortedMap.ValueTuple.addRange [| struct ("a", "b") |] nullTable + |> ignore + ) + "ValueTuple.setItems", + (fun () -> + SortedMap.ValueTuple.setItems [| struct ("a", "b") |] nullTable + |> ignore + ) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a null + /// sequence. + static let nullSequenceCalls : Map unit))> = + let table = defaultTable () + + Map.ofArray [| + nameof SortedMap.addRange, + struct ("pairs", + fun () -> + SortedMap.addRange (Unchecked.defaultof>>) table + |> ignore) + "ofArray of key/value pairs", + struct ("elements", + fun () -> + SortedMap.ofArray (Unchecked.defaultof array>) + |> ignore) + "ofArray of reference tuples", + struct ("elements", + fun () -> + SortedMap.ofArray (Unchecked.defaultof<(string * string) array>) + |> ignore) + "ofArray of struct tuples", + struct ("elements", + fun () -> + SortedMap.ofArray (Unchecked.defaultof) + |> ignore) + "ofSeq of key/value pairs", + struct ("elements", + fun () -> + SortedMap.ofSeq (Unchecked.defaultof>>) + |> ignore) + "ofSeq of reference tuples", + struct ("elements", + fun () -> + SortedMap.ofSeq (Unchecked.defaultof>) + |> ignore) + "ofSeq of struct tuples", + struct ("elements", + fun () -> + SortedMap.ofSeq (Unchecked.defaultof>) + |> ignore) + nameof SortedMap.ofSeqBy, + struct ("source", + fun () -> + SortedMap.ofSeqBy id (Unchecked.defaultof>) + |> ignore) + nameof SortedMap.removeRange, + struct ("keys", + fun () -> + SortedMap.removeRange (Unchecked.defaultof>) table + |> ignore) + nameof SortedMap.setItems, + struct ("pairs", + fun () -> + SortedMap.setItems (Unchecked.defaultof>>) table + |> ignore) + "Tuple.addRange", + struct ("pairs", + fun () -> + SortedMap.Tuple.addRange (Unchecked.defaultof>) table + |> ignore) + "Tuple.setItems", + struct ("pairs", + fun () -> + SortedMap.Tuple.setItems (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.addRange", + struct ("pairs", + fun () -> + SortedMap.ValueTuple.addRange (Unchecked.defaultof>) table + |> ignore) + "ValueTuple.setItems", + struct ("pairs", + fun () -> + SortedMap.ValueTuple.setItems (Unchecked.defaultof>) table + |> ignore) + |] + + /// The functions that take a sequence, by name, with the name of the sequence parameter and the function on a default + /// flat list that is given as the sequence. A default flat list converts to a sequence without a check and fails with + /// InvalidOperationException when the sequence is enumerated, so the other inputs are valid and the function would + /// enumerate it. The last row gives a sequence of objects, which a default flat list of strings is by the covariance of + /// seq: the element type of the sequence is not the one of the flat list. The keys of a sorted map need comparison, so + /// only removeRange takes the objects here, with the keys of a sorted map that a comparer of objects orders; ofSeqBy + /// takes them through the struct. + static let defaultSequenceCalls : Map unit))> = + let table = defaultTable () + + let objectTable : SortedMap = + (SortedMap.forKeyComparer Comparer.Default).ofArray [| KeyValuePair("one", "uno") |] + + let defaultPairs = Unchecked.defaultof>> :> seq<_> + let defaultTuples = Unchecked.defaultof> :> seq<_> + let defaultStructTuples = Unchecked.defaultof> :> seq<_> + let defaultKeys = Unchecked.defaultof> :> seq<_> + + Map.ofArray [| + nameof SortedMap.addRange, struct ("pairs", fun () -> SortedMap.addRange defaultPairs table |> ignore) + "ofSeq of key/value pairs", struct ("elements", fun () -> SortedMap.ofSeq defaultPairs |> ignore) + "ofSeq of reference tuples", struct ("elements", fun () -> SortedMap.ofSeq defaultTuples |> ignore) + "ofSeq of struct tuples", struct ("elements", fun () -> SortedMap.ofSeq defaultStructTuples |> ignore) + nameof SortedMap.ofSeqBy, struct ("source", fun () -> SortedMap.ofSeqBy id defaultKeys |> ignore) + nameof SortedMap.removeRange, struct ("keys", fun () -> SortedMap.removeRange defaultKeys table |> ignore) + nameof SortedMap.setItems, struct ("pairs", fun () -> SortedMap.setItems defaultPairs table |> ignore) + "Tuple.addRange", struct ("pairs", fun () -> SortedMap.Tuple.addRange defaultTuples table |> ignore) + "Tuple.setItems", struct ("pairs", fun () -> SortedMap.Tuple.setItems defaultTuples table |> ignore) + "ValueTuple.addRange", + struct ("pairs", + fun () -> + SortedMap.ValueTuple.addRange defaultStructTuples table + |> ignore) + "ValueTuple.setItems", + struct ("pairs", + fun () -> + SortedMap.ValueTuple.setItems defaultStructTuples table + |> ignore) + $"%s{nameof SortedMap.removeRange} (default flat list of strings as objects)", + struct ("keys", + fun () -> + SortedMap.removeRange FlatListOracles.defaultStringsAsObjects objectTable + |> ignore) + |] + + /// The creation functions without comparers, by name and shape of the bindings. + static let defaultCreations : Map SortedMap> = + Map.ofArray [| + nameof SortedMap.build, (fun () -> SortedMap.build (fun builder -> builder["a"] <- "b")) + nameof SortedMap.builder, (fun () -> SortedMap.ofBuilder (SortedMap.builder ())) + nameof SortedMap.empty, (fun () -> SortedMap.empty) + "ofArray of key/value pairs", (fun () -> SortedMap.ofArray [| KeyValuePair ("a", "b") |]) + "ofArray of reference tuples", (fun () -> SortedMap.ofArray [| ("a", "b") |]) + "ofArray of struct tuples", (fun () -> SortedMap.ofArray [| struct ("a", "b") |]) + "ofList of key/value pairs", (fun () -> SortedMap.ofList [ KeyValuePair ("a", "b") ]) + "ofList of reference tuples", (fun () -> SortedMap.ofList [ ("a", "b") ]) + "ofList of struct tuples", (fun () -> SortedMap.ofList [ struct ("a", "b") ]) + "ofSeq of key/value pairs", (fun () -> SortedMap.ofSeq (dict [| "a", "b" |])) + "ofSeq of reference tuples", (fun () -> SortedMap.ofSeq (Seq.singleton ("a", "b"))) + "ofSeq of struct tuples", (fun () -> SortedMap.ofSeq (Seq.singleton (struct ("a", "b")))) + nameof SortedMap.ofSeqBy, (fun () -> SortedMap.ofSeqBy id [| "a" |]) + nameof SortedMap.singleton, (fun () -> SortedMap.singleton "a" "b") + |] + + /// The creations of a sorted map of tuple keys by the functions, members and expressions that order the keys with F# + /// generic comparison, by name. F# creates a new comparer of tuples on each call of FastGenericComparer at the type. + static let tupleKeyCreations : Map SortedMap> = + Map.ofArray [| + "build", (fun () -> SortedMap.build (fun builder -> builder[(1, "a")] <- 1)) + "builder", (fun () -> SortedMap.ofBuilder (SortedMap.builder ())) + "ofArray of struct tuples", (fun () -> SortedMap.ofArray [| struct ((1, "a"), 1) |]) + "ofList of reference tuples", (fun () -> SortedMap.ofList [ (1, "a"), 1 ]) + "ofSeq of key/value pairs", (fun () -> SortedMap.ofSeq [| KeyValuePair ((1, "a"), 1) |]) + "ofSeqBy", (fun () -> SortedMap.ofSeqBy (fun (value : int) -> value, "a") [| 1 |]) + "singleton", (fun () -> SortedMap.singleton (1, "a") 1) + "sortedMap", (fun () -> sortedMap { yield struct ((1, "a"), 1) }) + |] + + /// The transformations that keep the type of the values, by name, on the map whose keys ignore the case. + static let sameTypeTransformations : Map -> SortedMap> = + Map.ofArray [| + nameof SortedMap.add, SortedMap.add "four" "cuatro" + nameof SortedMap.addRange, SortedMap.addRange [| KeyValuePair ("four", "cuatro") |] + nameof SortedMap.change, SortedMap.change "one" (fun _ -> ValueNone) + nameof SortedMap.clear, SortedMap.clear + nameof SortedMap.filter, SortedMap.filter (fun (key : string) _ -> key.Length > 3) + ($"%s{nameof SortedMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = SortedMap.partition (fun (key : string) _ -> key.Length > 3) table + accepted) + ($"%s{nameof SortedMap.partition} (rejected)", + fun table -> + let struct (_, rejected) = SortedMap.partition (fun (key : string) _ -> key.Length > 3) table + rejected) + nameof SortedMap.remove, SortedMap.remove "one" + nameof SortedMap.removeRange, SortedMap.removeRange [| "one"; "two" |] + nameof SortedMap.setItem, SortedMap.setItem "four" "cuatro" + nameof SortedMap.setItems, SortedMap.setItems [| KeyValuePair ("four", "cuatro") |] + nameof SortedMap.update, + SortedMap.update (fun (builder : SortedMap.Builder) -> builder["four"] <- "cuatro") + "Tuple.addRange", SortedMap.Tuple.addRange [| "four", "cuatro" |] + "Tuple.setItems", SortedMap.Tuple.setItems [| "four", "cuatro" |] + "ValueTuple.addRange", SortedMap.ValueTuple.addRange [| struct ("four", "cuatro") |] + "ValueTuple.setItems", SortedMap.ValueTuple.setItems [| struct ("four", "cuatro") |] + |] + + /// The transformations that change the type of the values, by name, on the map whose keys ignore the case. + static let typeChangingTransformations : Map -> SortedMap> = + Map.ofArray [| + nameof SortedMap.choose, SortedMap.choose (fun _ (value : string) -> ValueSome value.Length) + nameof SortedMap.map, SortedMap.map (fun _ (value : string) -> value.Length) + |] + + /// The transformations that change the type of the values, by name, on a map whose keys are ordered from the largest to + /// the smallest. + static let descendingTransformations : Map -> SortedMap> = + Map.ofArray [| + nameof SortedMap.choose, SortedMap.choose (fun _ (value : string) -> ValueSome value.Length) + nameof SortedMap.map, SortedMap.map (fun _ (value : string) -> value.Length) + |] + + /// The key comparer of the result of every transformation, by name. + static let resultKeyComparers : Map -> IComparer> = + Map.ofSeq ( + seq { + for KeyValue (name, transform) in sameTypeTransformations do + name, transform >> SortedMap.keyComparer + + for KeyValue (name, transform) in typeChangingTransformations do + name, transform >> SortedMap.keyComparer + } + ) + + /// The functions that change nothing in the map with the default comparers, by name. + static let unchangingCalls : Map -> SortedMap> = + Map.ofArray [| + nameof SortedMap.add, SortedMap.add "one" "uno" + nameof SortedMap.addRange, SortedMap.addRange [| KeyValuePair ("one", "uno") |] + $"%s{nameof SortedMap.change} of a key with a binding", SortedMap.change "one" id + $"%s{nameof SortedMap.change} of a key without a binding", SortedMap.change "three" id + nameof SortedMap.filter, SortedMap.filter (fun _ _ -> true) + ($"%s{nameof SortedMap.partition} (accepted)", + fun table -> + let struct (accepted, _) = SortedMap.partition (fun _ _ -> true) table + accepted) + nameof SortedMap.remove, SortedMap.remove "three" + nameof SortedMap.removeRange, SortedMap.removeRange [| "three" |] + nameof SortedMap.setItem, SortedMap.setItem "two" "dos" + nameof SortedMap.setItems, SortedMap.setItems [| KeyValuePair ("two", "dos") |] + nameof SortedMap.update, SortedMap.update ignore + "Tuple.addRange", SortedMap.Tuple.addRange [| "one", "uno" |] + "Tuple.setItems", SortedMap.Tuple.setItems [| "two", "dos" |] + "ValueTuple.addRange", SortedMap.ValueTuple.addRange [| struct ("one", "uno") |] + "ValueTuple.setItems", SortedMap.ValueTuple.setItems [| struct ("two", "dos") |] + |] + + /// The functions that bind "ONE" to "abc" in the map whose keys ignore the case and whose values compare by their + /// length, by name: the map binds "one" to "uno", a value that the value comparer finds equal to "abc". + static let equalValueCalls : Map -> SortedMap> = + Map.ofArray [| + nameof SortedMap.add, SortedMap.add "ONE" "abc" + nameof SortedMap.addRange, SortedMap.addRange [| KeyValuePair ("ONE", "abc") |] + nameof SortedMap.change, SortedMap.change "ONE" (fun _ -> ValueSome "abc") + nameof SortedMap.setItem, SortedMap.setItem "ONE" "abc" + nameof SortedMap.setItems, SortedMap.setItems [| KeyValuePair ("ONE", "abc") |] + "Tuple.addRange", SortedMap.Tuple.addRange [| "ONE", "abc" |] + "Tuple.setItems", SortedMap.Tuple.setItems [| "ONE", "abc" |] + "ValueTuple.addRange", SortedMap.ValueTuple.addRange [| struct ("ONE", "abc") |] + "ValueTuple.setItems", SortedMap.ValueTuple.setItems [| struct ("ONE", "abc") |] + |] + + /// Every set of several keys with each creation: the rows of the test of the order of the creation functions. + static member KeysAndCreations = seq { + for keys in OrderedKeys.Several do + for creation in OrderedCreation.All do + struct (keys, creation) + } + + /// The names of the functions that take a map, as the rows of their test. + static member NullTableCalls = nullTableCalls.Keys + + /// The names of the functions that take a sequence, as the rows of their test. + static member NullSequenceCalls = nullSequenceCalls.Keys + + /// The names of the functions that take a sequence and are given a default flat list, as the rows of their test. + static member DefaultSequenceCalls = defaultSequenceCalls.Keys + + /// The names of the creation functions without comparers, as the rows of their test. + static member DefaultCreations = defaultCreations.Keys + + /// The names of the creations of sorted maps of tuple keys, as the rows of the test of their shared comparer. + static member TupleKeyCreations = tupleKeyCreations.Keys + + /// The names of the transformations that keep the type of the values, as the rows of their test. + static member SameTypeTransformations = sameTypeTransformations.Keys + + /// The names of the transformations that change the type of the values, as the rows of their tests. + static member TypeChangingTransformations = typeChangingTransformations.Keys + + /// The names of every transformation, as the rows of the test of the key comparer. + static member Transformations = resultKeyComparers.Keys + + /// The names of the functions that change nothing, as the rows of their test. + static member UnchangingCalls = unchangingCalls.Keys + + /// The names of the functions that set a value that the value comparer finds equal, as the rows of their test. + static member EqualValueCalls = equalValueCalls.Keys + + [] + member _.``functions reject a null map`` (name : string) = + // Map throws NullReferenceException for a null map; this library checks its arguments (ADR 0001, D6) + let ex = + Assert.ThrowsExactly(Action nullTableCalls[name], $"SortedMap.%s{name} must reject a null map") + + Assert.AreEqual("table", ex.ParamName, $"SortedMap.%s{name} must name the map parameter") + + [] + member _.``functions reject a null sequence`` (name : string) = + let struct (paramName, call) = nullSequenceCalls[name] + + let ex = + Assert.ThrowsExactly(Action call, $"SortedMap.%s{name} must reject a null sequence") + + Assert.AreEqual(paramName, ex.ParamName, $"SortedMap.%s{name} must name the sequence parameter") + + [] + member _.``functions reject a default flat list given as a sequence`` (name : string) = + // The exception names the parameter of the function, not the one of the BCL member that it would have called + let struct (paramName, call) = defaultSequenceCalls[name] + FlatListAssert.RejectsNull ($"SortedMap.%s{name}", paramName, call) + + [] + member _.``a flat list that is not default passes as a sequence, also one of strings as a sequence of objects`` () = + let flatList (elements : 'T array) = FlatList.ofArray elements :> seq<'T> + let table = defaultTable () + + CollectionAssert.AreEqual ( + [| struct ("four", "cuatro"); struct ("one", "uno"); struct ("two", "dos") |], + SortedMap.toArray (SortedMap.addRange (flatList [| KeyValuePair ("four", "cuatro") |]) table), + "SortedMap.addRange must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno"); struct ("two", "dos"); struct ("zero", "cero") |], + SortedMap.toArray (SortedMap.setItems (flatList [| KeyValuePair ("zero", "cero") |]) table), + "SortedMap.setItems must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("two", "dos") |], + SortedMap.toArray (SortedMap.removeRange (flatList [| "one" |]) table), + "SortedMap.removeRange must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (SortedMap.ofSeq (flatList [| KeyValuePair ("a", "b") |])), + "SortedMap.ofSeq must take the key/value pairs of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (SortedMap.ofSeq (flatList [| ("a", "b") |])), + "SortedMap.ofSeq must take the reference tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("a", "b") |], + SortedMap.toArray (SortedMap.ofSeq (flatList [| struct ("a", "b") |])), + "SortedMap.ofSeq must take the struct tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("ab", "ab") |], + SortedMap.toArray (SortedMap.ofSeqBy id (flatList [| "ab" |])), + "SortedMap.ofSeqBy must take the elements of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("one", "uno"); struct ("two", "dos"); struct ("zero", "cero") |], + SortedMap.toArray (SortedMap.Tuple.setItems (flatList [| ("zero", "cero") |]) table), + "SortedMap.Tuple.setItems must take the tuples of a flat list" + ) + + CollectionAssert.AreEqual ( + [| struct ("four", "cuatro"); struct ("one", "uno"); struct ("two", "dos") |], + SortedMap.toArray (SortedMap.ValueTuple.addRange (flatList [| struct ("four", "cuatro") |]) table), + "SortedMap.ValueTuple.addRange must take the struct tuples of a flat list" + ) + + // A flat list of strings is a sequence of objects, which a type test for the flat list of objects does not see + let widened = FlatListOracles.stringsAsObjects (FlatList.ofArray [| "one" |]) + + let objectTable : SortedMap = + (SortedMap.forKeyComparer Comparer.Default).ofArray [| + KeyValuePair("one", "uno") + KeyValuePair("two", "dos") + |] + + CollectionAssert.AreEqual ( + [| "two" :> obj |], + SortedMap.keys (SortedMap.removeRange widened objectTable) + |> Seq.toArray, + "SortedMap.removeRange must take the elements of a flat list of strings as objects" + ) + + [] + member _.``the creation functions order the keys as Map does`` (keys : OrderedKeys, creation : OrderedCreation) = + let message = "The creation must order the keys as Map does" + + match keys with + | OrderedKeys.Strings -> inMapOrder (toMap stringBindings) (creation.Create stringBindings) message + | OrderedKeys.Negatives -> inMapOrder (toMap negativeBindings) (creation.Create negativeBindings) message + | OrderedKeys.Tuples -> inMapOrder (toMap tupleBindings) (creation.Create tupleBindings) message + | OrderedKeys.OneKey -> inMapOrder (toMap oneBinding) (creation.Create oneBinding) message + + [)>] + member _.``ofSeqBy orders the keys as Map does`` (keys : OrderedKeys) = + let message = "SortedMap.ofSeqBy must order the keys as Map does" + + match keys with + | OrderedKeys.Strings -> + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (toMap stringBindings)), + Seq.toArray (SortedMap.keys (SortedMap.ofSeqBy (fun struct (key, _) -> key) stringBindings)), + message + ) + | OrderedKeys.Negatives -> + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (toMap negativeBindings)), + Seq.toArray (SortedMap.keys (SortedMap.ofSeqBy (fun struct (key, _) -> key) negativeBindings)), + message + ) + | OrderedKeys.Tuples -> + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (toMap tupleBindings)), + Seq.toArray (SortedMap.keys (SortedMap.ofSeqBy (fun struct (key, _) -> key) tupleBindings)), + message + ) + | OrderedKeys.OneKey -> + CollectionAssert.AreEqual ( + Seq.toArray (Map.keys (toMap oneBinding)), + Seq.toArray (SortedMap.keys (SortedMap.ofSeqBy (fun struct (key, _) -> key) oneBinding)), + message + ) + + [)>] + member _.``the functions that visit the bindings follow the order of Map`` (keys : OrderedKeys) = + match keys with + | OrderedKeys.Strings -> assertVisitingOrder stringBindings + | OrderedKeys.Negatives -> assertVisitingOrder negativeBindings + | OrderedKeys.Tuples -> assertVisitingOrder tupleBindings + | OrderedKeys.OneKey -> assertVisitingOrder oneBinding + + [] + member _.``foldBack visits the keys from the largest to the smallest`` () = + let table = SortedMap.ofArray stringBindings + let visited = SortedMap.foldBack (fun key _ keys -> Array.append [| key |] keys) table [||] + let reversed = SortedMap.foldBack (fun key _ keys -> Array.append keys [| key |]) table [||] + + CollectionAssert.AreEqual ( + [| "10"; "9"; "A"; "B"; "_"; "a"; "b" |], + visited, + "SortedMap.foldBack must cons the keys into their ordinal order" + ) + + CollectionAssert.AreEqual ( + [| "b"; "a"; "_"; "B"; "A"; "9"; "10" |], + reversed, + "SortedMap.foldBack must visit the largest key first" + ) + + [)>] + member _.``the transformations that rebuild the map keep the order of Map`` (keys : OrderedKeys) = + match keys with + | OrderedKeys.Strings -> assertTransformationOrder stringBindings + | OrderedKeys.Negatives -> assertTransformationOrder negativeBindings + | OrderedKeys.Tuples -> assertTransformationOrder tupleBindings + | OrderedKeys.OneKey -> assertTransformationOrder oneBinding + + [)>] + member _.``minKeyValue returns the binding of the smallest key as Map does`` (keys : OrderedKeys) = + match keys with + | OrderedKeys.Strings -> assertMinimum stringBindings + | OrderedKeys.Negatives -> assertMinimum negativeBindings + | OrderedKeys.Tuples -> assertMinimum tupleBindings + | OrderedKeys.OneKey -> assertMinimum oneBinding + + [] + member _.``minKeyValue returns the first key in the order of the key comparer`` () = + Assert.AreEqual( + struct (8, "eight"), + SortedMap.minKeyValue ((SortedMap.forKeyComparer descending).ofArray negativeBindings), + "SortedMap.minKeyValue must return the first key in the order of the key comparer" + ) + + [)>] + member _.``maxKeyValue returns the binding of the largest key as Map does`` (keys : OrderedKeys) = + match keys with + | OrderedKeys.Strings -> assertMaximum stringBindings + | OrderedKeys.Negatives -> assertMaximum negativeBindings + | OrderedKeys.Tuples -> assertMaximum tupleBindings + | OrderedKeys.OneKey -> assertMaximum oneBinding + + [] + member _.``maxKeyValue returns the last key in the order of the key comparer`` () = + Assert.AreEqual( + struct (-12, "minus twelve"), + SortedMap.maxKeyValue ((SortedMap.forKeyComparer descending).ofArray negativeBindings), + "SortedMap.maxKeyValue must return the last key in the order of the key comparer" + ) + + [] + member _.``minKeyValue throws for an empty map as Map does`` () = + ExceptionAssert.Same ( + id, + (fun () -> Map.minKeyValue Map.empty |> ignore), + (fun () -> SortedMap.minKeyValue SortedMap.empty |> ignore), + "SortedMap.minKeyValue must reject an empty map" + ) + + [] + member _.``maxKeyValue throws for an empty map as Map does`` () = + ExceptionAssert.Same ( + id, + (fun () -> Map.maxKeyValue Map.empty |> ignore), + (fun () -> SortedMap.maxKeyValue SortedMap.empty |> ignore), + "SortedMap.maxKeyValue must reject an empty map" + ) + + [] + member _.``singleton binds the key to the value`` () = + // The module before ADR 0001 passed the argument of singleton as a comparer to ImmutableSortedDictionary.Create, + // which built an empty map + let table = SortedMap.singleton "key" "value" + Assert.AreEqual(1, SortedMap.count table, "SortedMap.singleton must create one binding") + Assert.AreEqual("value", SortedMap.find "key" table, "SortedMap.singleton must bind the key to the value") + + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + "SortedMap.singleton must order the keys with F# generic comparison" + ) + + [] + member _.``find throws the exception of Map.find, not the one of the indexer`` () = + let table = defaultTable () + + let ex = + Assert.ThrowsExactly( + Action (fun () -> SortedMap.find "three" table |> ignore), + "SortedMap.find must reject a key without a binding" + ) + + Assert.AreEqual( + KeyNotFoundException().Message, + ex.Message, + "SortedMap.find must throw with the message of Map.find, which does not name the key" + ) + + [] + member _.``the creation functions without a comparer use F# generic comparison and the default value comparer`` + (name : string) + = + let table = defaultCreations[name]() + + // The comparer of F# generic comparison, which Map uses, and not the culture-sensitive Comparer.Default + Assert.AreSame>( + LanguagePrimitives.FastGenericComparer, + SortedMap.keyComparer table, + $"SortedMap.%s{name} must order the keys with F# generic comparison" + ) + + Assert.AreSame>( + EqualityComparer.Default, + SortedMap.valueComparer table, + $"SortedMap.%s{name} must compare the values with the default comparer" + ) + + [] + member _.``the creation functions share one comparer of F# generic comparison per type of the keys`` (name : string) = + // Comparers created per call would give maps of one type of keys different comparers, and the BCL takes its + // shortcuts for two maps only when they have the same comparer (ADR 0001, D5) + Assert.AreSame ( + SortedMap.keyComparer SortedMap.empty, + SortedMap.keyComparer (tupleKeyCreations[name]()), + $"SortedMap.%s{name} must order its keys with the comparer that every sorted map of the type of its keys shares" + ) + + [] + member _.``empty is one map per type of the keys and the values`` () = + Assert.AreSame (SortedMap.empty, SortedMap.empty, "SortedMap.empty must be created once") + Assert.IsTrue (SortedMap.isEmpty SortedMap.empty, "SortedMap.empty must have no bindings") + + [] + member _.``ofSeqBy rejects a repeated key with another value and accepts a repeated element, as the BCL does`` () = + Assert.ThrowsExactly( + Action (fun () -> + SortedMap.ofSeqBy (fun (value : int) -> string (value % 2)) [| 1; 3 |] + |> ignore + ), + "SortedMap.ofSeqBy must reject a repeated key with another value, as ImmutableSortedDictionary.ToImmutableSortedDictionary does" + ) + |> ignore + + let table = SortedMap.ofSeqBy (fun (value : int) -> string value) [| 1; 1 |] + Assert.AreEqual(1, SortedMap.count table, "SortedMap.ofSeqBy must keep one binding of a repeated element") + + [] + member _.``ofSeqBy keeps the first of the elements of a key that the default comparer finds equal`` () = + // ImmutableSortedDictionary.ToImmutableSortedDictionary keeps the first binding of a key for an equal value + // (ADR 0001, D14) + let first = MapInputs.Copy "uno" + let table = SortedMap.ofSeqBy (fun (value : string) -> value.Length) [| first; MapInputs.Copy "uno" |] + + Assert.AreSame ( + first, + SortedMap.find 3 table, + "SortedMap.ofSeqBy must keep the first of the elements that the default comparer finds equal" + ) + + [] + member _.``the transformations keep the key comparer`` (name : string) = + Assert.AreSame>( + StringComparer.OrdinalIgnoreCase, + resultKeyComparers[name](customTable ()), + $"SortedMap.%s{name} must keep the key comparer" + ) + + [] + member _.``the transformations that keep the type of the values keep the value comparer`` (name : string) = + Assert.AreSame ( + byLength, + SortedMap.valueComparer (sameTypeTransformations[name](customTable ())), + $"SortedMap.%s{name} must keep the value comparer" + ) + + [] + member _.``map and choose keep the key comparer and give the new values the default comparer`` (name : string) = + let result = + descendingTransformations[name]((SortedMap.forKeyComparer descending).ofArray negativeBindings) + + Assert.AreSame>(descending, SortedMap.keyComparer result, $"SortedMap.%s{name} must keep the key comparer") + + CollectionAssert.AreEqual ( + [| 8; 5; 0; -3; -12 |], + Seq.toArray (SortedMap.keys result), + $"SortedMap.%s{name} must keep the order of the key comparer" + ) + + Assert.AreSame>( + EqualityComparer.Default, + SortedMap.valueComparer result, + $"SortedMap.%s{name} must compare the new values with the default comparer" + ) + + [] + member _.``containsValue compares with the value comparer`` () = + let table = customTable () + Assert.IsTrue (SortedMap.containsValue "abc" table, "SortedMap.containsValue must find a value of the same length") + Assert.IsTrue (SortedMap.containsValue "tres" table, "SortedMap.containsValue must find an equal value") + Assert.IsFalse (SortedMap.containsValue "abcdef" table, "SortedMap.containsValue must not find a value of another length") + + [] + member _.``tryGetKey returns the key that the map holds`` () = + let table = customTable () + + Assert.AreEqual( + ValueSome "Two", + SortedMap.tryGetKey "TWO" table, + "SortedMap.tryGetKey must return the stored key" + ) + + Assert.AreEqual( + ValueNone, + SortedMap.tryGetKey "four" table, + "SortedMap.tryGetKey must return nothing for a missing key" + ) + + [] + member _.``keys and values are read-only views that ask the map`` () = + let table = customTable () + let keys = SortedMap.keys table + Assert.HasCount (3, keys, "SortedMap.keys must have a key per binding") + Assert.IsTrue (keys.Contains "THREE", "SortedMap.keys must look the keys up with the key comparer") + Assert.IsTrue (keys.IsReadOnly, "SortedMap.keys must be read-only") + + Assert.ThrowsExactly(Action (fun () -> keys.Add "four"), "SortedMap.keys must reject a change") + |> ignore + + let values = SortedMap.values table + Assert.HasCount (3, values, "SortedMap.values must have a value per binding") + Assert.IsTrue (values.Contains "xyz", "SortedMap.values must look the values up with the value comparer") + Assert.IsTrue (values.IsReadOnly, "SortedMap.values must be read-only") + + [] + member _.``the functions that change nothing return the input itself`` (name : string) = + let table = defaultTable () + + Assert.AreSame ( + table, + unchangingCalls[name] table, + $"SortedMap.%s{name} must return the input itself when it changes nothing" + ) + + [] + member _.``a value that the value comparer finds equal leaves the binding as it is`` (name : string) = + // ImmutableSortedDictionary.SetItem and AddRange keep the binding, its key included, for an equal value (ADR 0001, D14) + let table = customTable () + let result = equalValueCalls[name] table + Assert.AreEqual("uno", SortedMap.find "one" result, $"SortedMap.%s{name} must keep the current value") + + Assert.AreEqual( + ValueSome "one", + SortedMap.tryGetKey "ONE" result, + $"SortedMap.%s{name} must keep the current key" + ) + + Assert.AreSame (table, result, $"SortedMap.%s{name} must return the input itself") + + [] + member _.``clear returns an empty map itself`` () = + let empty = SortedMap.clear (defaultTable ()) + Assert.AreSame (empty, SortedMap.clear empty, "SortedMap.clear must return an empty input itself") + + [] + member _.``add throws what ImmutableSortedDictionary.Add throws for a key that is bound to another value`` () = + let table = defaultTable () + + ExceptionAssert.Same ( + id, + (fun () -> table.Add ("one", "other") |> ignore), + (fun () -> SortedMap.add "one" "other" table |> ignore), + "SortedMap.add must throw what ImmutableSortedDictionary.Add throws" + ) + + [] + member _.``add rejects a value that the value comparer finds different`` () = + // The comparer finds "uno" and "longer" different, as it finds "uno" and "abc" equal + Assert.ThrowsExactly( + Action (fun () -> SortedMap.add "ONE" "longer" (customTable ()) |> ignore), + "SortedMap.add must reject a value that the value comparer finds different from the current one" + ) + |> ignore + + [] + member _.``setItem replaces the value of a key that add rejects`` () = + let table = defaultTable () + let replaced = SortedMap.setItem "one" "other" table + Assert.AreEqual("other", SortedMap.find "one" replaced, "SortedMap.setItem must replace the value of the key") + Assert.AreEqual("uno", SortedMap.find "one" table, "SortedMap.setItem must not change the input") + + [] + member _.``containsBinding compares the value with the value comparer where tryFind and equality do not`` () = + let table = + (SortedMap.forComparers StringComparer.Ordinal StringComparer.OrdinalIgnoreCase).ofArray [| "one", "Uno" |] + + Assert.IsTrue ( + SortedMap.containsBinding "one" "UNO" table, + "SortedMap.containsBinding must find a value that the value comparer finds equal" + ) + + Assert.IsFalse ( + SortedMap.tryFind "one" table + |> ValueOption.exists (fun stored -> stored = "UNO"), + "F# equality finds \"Uno\" and \"UNO\" different, which is why the function asks the value comparer" + ) + + Assert.IsFalse (SortedMap.containsBinding "one" "DOS" table, "SortedMap.containsBinding must not find another value") + + Assert.IsFalse ( + SortedMap.containsBinding "ONE" "Uno" table, + "SortedMap.containsBinding must look the key up with the key comparer" + ) + + Assert.IsFalse ( + SortedMap.containsBinding "two" "Uno" table, + "SortedMap.containsBinding must not find a key without a binding" + ) + + [] + member _.``containsBinding finds the key in the key comparer and the value in the value comparer`` + (key : string, value : string, expected : bool) + = + // The keys ignore the case and the values compare by their length: "one" is bound to "uno" + Assert.AreEqual( + expected, + SortedMap.containsBinding key value (customTable ()), + "SortedMap.containsBinding must compare the key and the value with the comparers of the map" + ) + + [] + member _.``containsBinding throws for a null key as ImmutableSortedDictionary.Contains does`` () = + let nullKey = Unchecked.defaultof + let table = defaultTable () + + // The member names its parameter Key, where ContainsKey names it key + ExceptionAssert.Same ( + id, + (fun () -> table.Contains (KeyValuePair (nullKey, "uno")) |> ignore), + (fun () -> SortedMap.containsBinding nullKey "uno" table |> ignore), + "SortedMap.containsBinding must throw what ImmutableSortedDictionary.Contains throws for a null key" + ) + + Assert.ThrowsExactly( + Action (fun () -> SortedMap.containsKey nullKey table |> ignore), + "SortedMap.containsKey must reject a null key too" + ) + |> ignore + + [] + member _.``tryFind followed by defaultValue gives what GetValueOrDefault gives`` (key : string) = + let table = defaultTable () + + Assert.AreEqual( + ImmutableDictionary.GetValueOrDefault (table, key, "fallback"), + SortedMap.tryFind key table + |> ValueOption.defaultValue "fallback", + "tryFind followed by defaultValue must give the value of the key, or the default value of a key without a binding" + ) + + [] + member _.``change with a function that keeps an existing value adds a binding only when the key has none`` + (key : string, expected : string) + = + let table = defaultTable () + + let added = + SortedMap.change + key + (function + | ValueNone -> ValueSome "tres" + | existing -> existing) + table + + Assert.AreEqual( + expected, + SortedMap.find key added, + "SortedMap.change must keep the value of a key with a binding and add the value of a key without one" + ) + + [] + member _.``addRange to an empty map rejects a repeated key with another value as CreateRange does`` () = + let pairs = [| KeyValuePair ("a", "x"); KeyValuePair ("a", "y") |] + + ExceptionAssert.Same ( + id, + (fun () -> ImmutableSortedDictionary.CreateRange pairs |> ignore), + (fun () -> SortedMap.empty |> SortedMap.addRange pairs |> ignore), + "SortedMap.addRange must reject what ImmutableSortedDictionary.CreateRange rejects" + ) + + Assert.AreEqual( + "y", + SortedMap.find "a" (SortedMap.ofSeq pairs), + "SortedMap.ofSeq must keep the last binding of a repeated key, where CreateRange throws" + ) + + [] + member _.``addRange of the mapped pairs rejects what ToImmutableSortedDictionary with an element selector rejects`` () = + // The keys are the lengths of the texts, the elements are the upper case texts: "a" and "b" share a key + let source = [| "a"; "b"; "cc" |] + let key (text : string) = text.Length + let element (text : string) = text.ToUpperInvariant () + let pairs = + source + |> Seq.map (fun text -> KeyValuePair (key text, element text)) + + ExceptionAssert.Same ( + id, + (fun () -> + ImmutableSortedDictionary.ToImmutableSortedDictionary ( + source, + Func key, + Func element + ) + |> ignore + ), + (fun () -> SortedMap.empty |> SortedMap.addRange pairs |> ignore), + "SortedMap.addRange of the mapped pairs must reject a repeated key like ToImmutableSortedDictionary does" + ) + + Assert.AreEqual( + "B", + SortedMap.find 1 (SortedMap.ofSeq pairs), + "SortedMap.ofSeq of the mapped pairs must keep the last binding of a repeated key" + ) + + [] + member _.``addRange of the mapped pairs accepts a repeated key with an equal element as ToImmutableSortedDictionary does`` + () + = + let source = [| "a"; "a"; "cc" |] + let key (text : string) = text.Length + let element (text : string) = text.ToUpperInvariant () + let pairs = + source + |> Seq.map (fun text -> KeyValuePair (key text, element text)) + + let expected = + ImmutableSortedDictionary.ToImmutableSortedDictionary (source, Func key, Func element) + + CollectionAssert.AreEqual ( + Seq.toArray expected, + Seq.toArray (SortedMap.empty |> SortedMap.addRange pairs), + "SortedMap.addRange of the mapped pairs must accept a repeated key with an equal element, as ToImmutableSortedDictionary does" + ) diff --git a/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTupleTests.fs b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTupleTests.fs new file mode 100644 index 00000000..a902d5a8 --- /dev/null +++ b/tests/FSharp.Collections.Immutable.Tests/SortedMap/SortedMapTupleTests.fs @@ -0,0 +1,168 @@ +namespace FSharp.Collections.Immutable.Tests + +open System.Collections.Generic +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 `SortedMap` module of the `Tuple` namespace. +[] +type SortedMapTupleTests () = + + /// The bindings of the inputs as reference pairs. + static let pairs (bindings : struct (int * string) array) = bindings |> Array.map MapInputs.ToTuple + + static let table = FSharp.Collections.Immutable.SortedMap.ofSeq MapInputs.Several + + static let oracle = Map.ofArray (pairs MapInputs.Several) + + /// The creations of a sorted map from reference tuples, by the member of the extensions that they reach; the type + /// annotation proves that each one creates a sorted map. + static let creations : Map SortedMap> = + Map.ofArray [| + "ofArray", (fun bindings -> SortedMap.ofArray bindings) + // The member under test takes a list + "ofList", (fun bindings -> SortedMap.ofList (List.ofArray bindings)) + "ofSeq", (fun bindings -> bindings |> SortedMap.ofSeq) + |] + + /// The changes of the sorted map by the functions of the nested modules of the default module, by their names; each one + /// adds the bindings of 1 and 100. + static let nestedModuleChanges : Map -> SortedMap> = + Map.ofArray [| + "Tuple.addRange", SortedMap.Tuple.addRange [| 1, "one"; 100, "hundred" |] + "Tuple.setItems", SortedMap.Tuple.setItems [| 1, "one"; 100, "hundred" |] + "ValueTuple.addRange", SortedMap.ValueTuple.addRange [| struct (1, "one"); struct (100, "hundred") |] + "ValueTuple.setItems", SortedMap.ValueTuple.setItems [| struct (1, "one"); struct (100, "hundred") |] + |] + + /// The names of the creations from reference tuples, as the rows of the test of the creation members. + static member Creations = creations.Keys + + /// The names of the functions of the nested modules, as the rows of the test of their resolution. + static member NestedModuleChanges = nestedModuleChanges.Keys + + [] + member _.``maxKeyValue returns a reference tuple as Map.maxKeyValue does`` () = + let largest : int * string = SortedMap.maxKeyValue table + Assert.AreEqual(Map.maxKeyValue oracle, largest, "Tuple.SortedMap.maxKeyValue must match Map.maxKeyValue") + + OverrideAssert.PairMatches ( + FSharp.Collections.Immutable.SortedMap.maxKeyValue table, + largest, + "Tuple.SortedMap.maxKeyValue must match the default" + ) + + ExceptionAssert.Same ( + id, + (fun () -> Map.maxKeyValue Map.empty |> ignore), + (fun () -> + SortedMap.maxKeyValue FSharp.Collections.Immutable.SortedMap.empty + |> ignore + ), + "Tuple.SortedMap.maxKeyValue must reject an empty map as Map.maxKeyValue does" + ) + + [] + member _.``minKeyValue returns a reference tuple as Map.minKeyValue does`` () = + let smallest : int * string = SortedMap.minKeyValue table + Assert.AreEqual(Map.minKeyValue oracle, smallest, "Tuple.SortedMap.minKeyValue must match Map.minKeyValue") + + OverrideAssert.PairMatches ( + FSharp.Collections.Immutable.SortedMap.minKeyValue table, + smallest, + "Tuple.SortedMap.minKeyValue must match the default" + ) + + ExceptionAssert.Same ( + id, + (fun () -> Map.minKeyValue Map.empty |> ignore), + (fun () -> + SortedMap.minKeyValue FSharp.Collections.Immutable.SortedMap.empty + |> ignore + ), + "Tuple.SortedMap.minKeyValue must reject an empty map as Map.minKeyValue does" + ) + + [] + member _.``partition returns a reference tuple`` () = + let (accepted : SortedMap), (rejected : SortedMap) = + SortedMap.partition (fun key _ -> key > 0) table + + let expectedAccepted, expectedRejected = Map.partition (fun key _ -> key > 0) oracle + + CollectionAssert.AreEqual ( + Map.toArray expectedAccepted, + SortedMap.toArray accepted, + "Tuple.SortedMap.partition must return the accepted bindings" + ) + + CollectionAssert.AreEqual ( + Map.toArray expectedRejected, + SortedMap.toArray rejected, + "Tuple.SortedMap.partition must return the rejected bindings" + ) + + [] + member _.``toArray returns reference tuples in the order of the keys`` () = + let array : (int * string) array = SortedMap.toArray table + + CollectionAssert.AreEqual (Map.toArray oracle, array, "Tuple.SortedMap.toArray must follow the order of Map.toArray") + + [] + member _.``toList returns reference tuples in the order of the keys`` () = + let list : (int * string) list = SortedMap.toList table + + CollectionAssert.AreEqual ( + Map.toArray oracle, + List.toArray list, + "Tuple.SortedMap.toList must follow the order of Map.toList" + ) + + [] + member _.``toSeq returns reference tuples in the order of the keys`` () = + let sequence : seq = SortedMap.toSeq table + + CollectionAssert.AreEqual ( + Seq.toArray (Map.toSeq oracle), + Seq.toArray sequence, + "Tuple.SortedMap.toSeq must follow the order of Map.toSeq" + ) + + [] + member _.``the functions without a tuple keep resolving to the default module`` () = + let found : string voption = SortedMap.tryFind 5 table + let replacements : KeyValuePair array = [| KeyValuePair (5, "five again") |] + let set : SortedMap = SortedMap.setItems replacements table + Assert.AreEqual(ValueSome "five", found, "SortedMap.tryFind must resolve to the default module") + + Assert.AreEqual( + "five again", + FSharp.Collections.Immutable.SortedMap.find 5 set, + "SortedMap.setItems must resolve to the default module" + ) + + [] + member _.``the creation members keep resolving to the extensions of the type`` (creation : string) = + // The module of the namespace has no ofArray, ofList or ofSeq, so they reach the overloads of SortedMapExtensions + let bindings = pairs MapInputs.RepeatedKeys + + CollectionAssert.AreEqual ( + Map.toArray (Map.ofArray bindings), + SortedMap.toArray (creations[creation] bindings), + $"SortedMap.%s{creation} must take reference tuples and keep the last binding of a key in the order of the keys" + ) + + [] + member _.``the nested modules of the default module resolve after opening the namespace`` (change : string) = + // The module of the namespace has no nested modules, so SortedMap.Tuple and SortedMap.ValueTuple reach the default + // module + let changed : SortedMap = nestedModuleChanges[change] table + + CollectionAssert.AreEqual ( + Map.toArray (oracle |> Map.add 1 "one" |> Map.add 100 "hundred"), + SortedMap.toArray changed, + $"SortedMap.%s{change} must add the bindings in the order of the keys" + )