diff --git a/apiCount.include.md b/apiCount.include.md index 99bea8de..60540fc5 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,28 +1,28 @@ -**API count: 937** +**API count: 941** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 907 | -| `net462` | 907 | -| `net47` | 906 | -| `net471` | 905 | -| `net472` | 901 | -| `net48` | 901 | -| `net481` | 901 | -| `netstandard2.0` | 903 | -| `netstandard2.1` | 734 | -| `netcoreapp2.0` | 827 | -| `netcoreapp2.1` | 746 | -| `netcoreapp2.2` | 746 | -| `netcoreapp3.0` | 692 | -| `netcoreapp3.1` | 691 | -| `net5.0` | 563 | -| `net6.0` | 468 | -| `net7.0` | 315 | -| `net8.0` | 197 | +| `net461` | 914 | +| `net462` | 914 | +| `net47` | 913 | +| `net471` | 912 | +| `net472` | 908 | +| `net48` | 908 | +| `net481` | 908 | +| `netstandard2.0` | 910 | +| `netstandard2.1` | 741 | +| `netcoreapp2.0` | 834 | +| `netcoreapp2.1` | 753 | +| `netcoreapp2.2` | 753 | +| `netcoreapp3.0` | 699 | +| `netcoreapp3.1` | 698 | +| `net5.0` | 570 | +| `net6.0` | 475 | +| `net7.0` | 322 | +| `net8.0` | 204 | | `net9.0` | 128 | | `net10.0` | 76 | | `net11.0` | 57 | -| `uap10.0` | 893 | +| `uap10.0` | 900 | diff --git a/api_list.include.md b/api_list.include.md index cac42e88..9aa72d2e 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -2,18 +2,24 @@ #### ArgumentException +> Requires [`true`](#argumentexception-1) in the consuming project. + * `void ThrowIfNullOrEmpty(string?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorempty?view=net-11.0#system-argumentexception-throwifnullorempty(system-string-system-string)) * `void ThrowIfNullOrWhiteSpace(string?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentexception.throwifnullorwhitespace?view=net-11.0#system-argumentexception-throwifnullorwhitespace(system-string-system-string)) #### ArgumentNullException +> Requires [`true`](#argumentexception-1) in the consuming project. + * `void ThrowIfNull(object?)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-11.0#system-argumentnullexception-throwifnull(system-object-system-string)) * `void ThrowIfNull(void*)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentnullexception.throwifnull?view=net-11.0#system-argumentnullexception-throwifnull(system-void*-system-string)) #### ArgumentOutOfRangeException +> Requires [`true`](#argumentexception-1) in the consuming project. + * `void ThrowIfEqual(T, T) where T : IEquatable?` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifequal?view=net-11.0#system-argumentoutofrangeexception-throwifequal-1(-0-0-system-string)) * `void ThrowIfGreaterThan(nint, nint)` * `void ThrowIfGreaterThan(T, T) where T : IComparable` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.argumentoutofrangeexception.throwifgreaterthan?view=net-11.0#system-argumentoutofrangeexception-throwifgreaterthan-1(-0-0-system-string)) @@ -222,8 +228,16 @@ #### Dictionary * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0) + * Note: No-op on older targets; the BCL grows the backing storage. + * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). + * Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. * `void TrimExcess()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess) + * Note: No-op on older targets; the BCL shrinks the backing storage. + * `bool TryGetAlternateLookup(DictionaryAlternateLookup) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). #### DictionaryEntry @@ -398,7 +412,14 @@ #### HashSet * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. + * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). + * Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. + * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). * `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0) @@ -598,8 +619,11 @@ * `void AddRange(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.addrange?view=net-11.0) * `void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.copyto?view=net-11.0) * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. + * Note: Returns void on older targets; the BCL returns int (the new capacity). * `void InsertRange(int, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0) * `void TrimExcess()` + * Note: No-op on older targets; the BCL shrinks the backing storage. #### Math @@ -647,6 +671,8 @@ #### ObjectDisposedException +> Requires [`true`](#argumentexception-1) in the consuming project. + * `void ThrowIf(bool, object)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception.throwif?view=net-11.0##system-objectdisposedexception-throwif(system-boolean-system-object)) * `void ThrowIf(bool, Type)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.objectdisposedexception.throwif?view=net-11.0##system-objectdisposedexception-throwif(system-boolean-system-type)) @@ -738,7 +764,9 @@ #### Queue * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. #### Random @@ -988,7 +1016,9 @@ #### Stack * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0) + * Note: No-op on older targets; the BCL grows the backing storage. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. * `bool TryPeek(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypeek?view=net-11.0) * `bool TryPop(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypop?view=net-11.0) @@ -1308,6 +1338,8 @@ #### Ensure +> Requires [`true`](#ensure-1) in the consuming project. + * `void DirectoryExists(string)` * `T Equal(T, T)` * `void FileExists(string)` diff --git a/assemblySize.include.md b/assemblySize.include.md index 46c2f81e..186d7549 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -2,24 +2,24 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 306.5KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netstandard2.1 | 8.5KB | 259.5KB | +251.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net461 | 8.5KB | 305.0KB | +296.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net462 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net47 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net471 | 8.5KB | 307.5KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net472 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | -| net48 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net481 | 8.5KB | 306.5KB | +298.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 282.5KB | +273.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.1 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +7.0KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 253.5KB | +244.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 251.5KB | +242.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net5.0 | 9.5KB | 214.5KB | +205.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | -| net6.0 | 10.0KB | 153.0KB | +143.0KB | +10.0KB | +7.0KB | +512bytes | +4.0KB | -| net7.0 | 10.0KB | 118.0KB | +108.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 89.5KB | +80.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 310.5KB | +302.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netstandard2.1 | 8.5KB | 263.5KB | +255.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net461 | 8.5KB | 309.0KB | +300.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net462 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net47 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net471 | 8.5KB | 311.5KB | +303.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net472 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | +| net48 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | +| net481 | 8.5KB | 310.5KB | +302.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 286.5KB | +277.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp2.1 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | +| netcoreapp2.2 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | +| netcoreapp3.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 255.5KB | +246.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net5.0 | 9.5KB | 218.5KB | +209.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | +| net6.0 | 10.0KB | 157.0KB | +147.0KB | +12.5KB | +10.0KB | +1.0KB | +4.0KB | +| net7.0 | 10.0KB | 122.0KB | +112.0KB | +9.0KB | +5.5KB | +512bytes | +4.0KB | +| net8.0 | 9.5KB | 94.0KB | +84.5KB | +8.5KB | | +512bytes | +3.5KB | | net9.0 | 9.5KB | 47.5KB | +38.0KB | +9.0KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 24.0KB | +14.0KB | +9.0KB | | +512bytes | +3.5KB | | net11.0 | 10.0KB | 18.5KB | +8.5KB | +9.0KB | | +512bytes | +3.5KB | @@ -29,24 +29,24 @@ | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 446.3KB | +438.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netstandard2.1 | 8.5KB | 374.1KB | +365.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net461 | 8.5KB | 445.8KB | +437.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net462 | 7.0KB | 449.3KB | +442.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net47 | 7.0KB | 449.1KB | +442.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net471 | 8.5KB | 447.7KB | +439.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net472 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | -| net48 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net481 | 8.5KB | 445.6KB | +437.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 412.4KB | +403.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.1 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.7KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 363.0KB | +353.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 360.9KB | +351.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net5.0 | 9.5KB | 305.8KB | +296.3KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | -| net6.0 | 10.0KB | 224.7KB | +214.7KB | +17.7KB | +8.7KB | +1.1KB | +4.7KB | -| net7.0 | 10.0KB | 170.9KB | +160.9KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 127.6KB | +118.1KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 453.3KB | +445.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netstandard2.1 | 8.5KB | 381.2KB | +372.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net461 | 8.5KB | 452.9KB | +444.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net462 | 7.0KB | 456.4KB | +449.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net47 | 7.0KB | 456.1KB | +449.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net471 | 8.5KB | 454.8KB | +446.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net472 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | +| net48 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | +| net481 | 8.5KB | 452.7KB | +444.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 419.5KB | +410.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp2.1 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | +| netcoreapp2.2 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | +| netcoreapp3.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 368.1KB | +358.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 313.0KB | +303.5KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | +| net6.0 | 10.0KB | 231.9KB | +221.9KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | +| net7.0 | 10.0KB | 178.3KB | +168.3KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB | +| net8.0 | 9.5KB | 135.4KB | +125.9KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 68.6KB | +59.1KB | +16.5KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 36.5KB | +26.5KB | +16.5KB | | +1.1KB | +4.2KB | | net11.0 | 10.0KB | 27.4KB | +17.4KB | +16.5KB | | +1.1KB | +4.2KB | diff --git a/claude.md b/claude.md index aede5701..8831aacc 100644 --- a/claude.md +++ b/claude.md @@ -69,6 +69,8 @@ Polyfill uses extensive `#if` directives. Key constants: - Each `*Polyfill.cs` file must contain exactly **one top-level type** (the `Polyfill` partial class). Helper classes must be **nested** inside `Polyfill`, otherwise `ReadMethodsForFiles` in `BuildApiTest` will throw. - The filename of static polyfill files directly determines the `api_list.include.md` section header: `{TypeName}Polyfill.cs` → `#### {TypeName}`. For example, `FilePolyfill.cs` → `#### File`. Choose filenames to match the type being extended. - `//Link:` comments on public methods must use `?view=net-11.0` for learn.microsoft.com URLs (enforced by `LinkReader`). For overloaded methods, include the `#fragment` anchor pointing to the specific overload (e.g., `#system-type-method(system-string-system-int32)`). +- `//Note: ` comments (also leading trivia, parsed by `LinkReader.GetNotes`) attach a caveat to a polyfilled API. Each line emits a sub-bullet `* Note: ` under the signature in `api_list.include.md` (rendered by `BuildApiTest.WriteNotes`). Use sparingly — only when behavior diverges from the BCL in a way callers must know about (e.g., O(n) vs O(1), free-standing struct vs nested type, different exception type). Don't restate the doc summary. +- Section-level opt-in gates (e.g., `Ensure`, `ArgumentNullException`) are emitted as a `> Requires true` blockquote under the section header. The mapping lives in `BuildApiTest.sectionGates` — add an entry there when you introduce a new section whose members all require an MSBuild flag. ### Test Projects diff --git a/readme.md b/readme.md index a107e5f6..2f901026 100644 --- a/readme.md +++ b/readme.md @@ -13,34 +13,34 @@ The package targets `netstandard2.0` and is designed to support the following ru * `uap10` -**API count: 937** +**API count: 941** ### Per Target Framework | Target | APIs | | -- | -- | -| `net461` | 907 | -| `net462` | 907 | -| `net47` | 906 | -| `net471` | 905 | -| `net472` | 901 | -| `net48` | 901 | -| `net481` | 901 | -| `netstandard2.0` | 903 | -| `netstandard2.1` | 734 | -| `netcoreapp2.0` | 827 | -| `netcoreapp2.1` | 746 | -| `netcoreapp2.2` | 746 | -| `netcoreapp3.0` | 692 | -| `netcoreapp3.1` | 691 | -| `net5.0` | 563 | -| `net6.0` | 468 | -| `net7.0` | 315 | -| `net8.0` | 197 | +| `net461` | 914 | +| `net462` | 914 | +| `net47` | 913 | +| `net471` | 912 | +| `net472` | 908 | +| `net48` | 908 | +| `net481` | 908 | +| `netstandard2.0` | 910 | +| `netstandard2.1` | 741 | +| `netcoreapp2.0` | 834 | +| `netcoreapp2.1` | 753 | +| `netcoreapp2.2` | 753 | +| `netcoreapp3.0` | 699 | +| `netcoreapp3.1` | 698 | +| `net5.0` | 570 | +| `net6.0` | 475 | +| `net7.0` | 322 | +| `net8.0` | 204 | | `net9.0` | 128 | | `net10.0` | 76 | | `net11.0` | 57 | -| `uap10.0` | 893 | +| `uap10.0` | 900 | @@ -96,24 +96,24 @@ This project uses features from the current stable SDK and C# language. As such | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 306.5KB | +298.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netstandard2.1 | 8.5KB | 259.5KB | +251.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net461 | 8.5KB | 305.0KB | +296.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net462 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net47 | 7.0KB | 308.5KB | +301.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net471 | 8.5KB | 307.5KB | +299.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net472 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | -| net48 | 8.5KB | 306.0KB | +297.5KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | -| net481 | 8.5KB | 306.5KB | +298.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.0 | 9.0KB | 282.5KB | +273.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| netcoreapp2.1 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp2.2 | 9.0KB | 262.0KB | +253.0KB | +9.0KB | +7.0KB | +9.0KB | +13.5KB | -| netcoreapp3.0 | 9.5KB | 253.5KB | +244.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | -| netcoreapp3.1 | 9.5KB | 251.5KB | +242.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | -| net5.0 | 9.5KB | 214.5KB | +205.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | -| net6.0 | 10.0KB | 153.0KB | +143.0KB | +10.0KB | +7.0KB | +512bytes | +4.0KB | -| net7.0 | 10.0KB | 118.0KB | +108.0KB | +9.0KB | +5.5KB | +512bytes | +3.5KB | -| net8.0 | 9.5KB | 89.5KB | +80.0KB | +8.5KB | +512bytes | +512bytes | +3.5KB | +| netstandard2.0 | 8.0KB | 310.5KB | +302.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netstandard2.1 | 8.5KB | 263.5KB | +255.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net461 | 8.5KB | 309.0KB | +300.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net462 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net47 | 7.0KB | 312.5KB | +305.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net471 | 8.5KB | 311.5KB | +303.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| net472 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | +| net48 | 8.5KB | 310.0KB | +301.5KB | +9.0KB | +7.0KB | +9.5KB | +14.0KB | +| net481 | 8.5KB | 310.5KB | +302.0KB | +8.5KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp2.0 | 9.0KB | 286.5KB | +277.5KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | +| netcoreapp2.1 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | +| netcoreapp2.2 | 9.0KB | 266.0KB | +257.0KB | +9.0KB | +7.0KB | +9.0KB | +14.0KB | +| netcoreapp3.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +13.5KB | +| netcoreapp3.1 | 9.5KB | 255.5KB | +246.0KB | +9.0KB | +6.5KB | +9.5KB | +14.0KB | +| net5.0 | 9.5KB | 218.5KB | +209.0KB | +9.5KB | +7.0KB | +9.5KB | +14.5KB | +| net6.0 | 10.0KB | 157.0KB | +147.0KB | +12.5KB | +10.0KB | +1.0KB | +4.0KB | +| net7.0 | 10.0KB | 122.0KB | +112.0KB | +9.0KB | +5.5KB | +512bytes | +4.0KB | +| net8.0 | 9.5KB | 94.0KB | +84.5KB | +8.5KB | | +512bytes | +3.5KB | | net9.0 | 9.5KB | 47.5KB | +38.0KB | +9.0KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 24.0KB | +14.0KB | +9.0KB | | +512bytes | +3.5KB | | net11.0 | 10.0KB | 18.5KB | +8.5KB | +9.0KB | | +512bytes | +3.5KB | @@ -123,24 +123,24 @@ This project uses features from the current stable SDK and C# language. As such | | Empty Assembly | With Polyfill | Diff | Ensure | ArgumentExceptions | StringInterpolation | Nullability | |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| -| netstandard2.0 | 8.0KB | 446.3KB | +438.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netstandard2.1 | 8.5KB | 374.1KB | +365.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net461 | 8.5KB | 445.8KB | +437.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net462 | 7.0KB | 449.3KB | +442.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net47 | 7.0KB | 449.1KB | +442.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net471 | 8.5KB | 447.7KB | +439.2KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net472 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | -| net48 | 8.5KB | 445.1KB | +436.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net481 | 8.5KB | 445.6KB | +437.1KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.0 | 9.0KB | 412.4KB | +403.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| netcoreapp2.1 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp2.2 | 9.0KB | 380.8KB | +371.8KB | +16.7KB | +8.7KB | +13.9KB | +18.9KB | -| netcoreapp3.0 | 9.5KB | 363.0KB | +353.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 360.9KB | +351.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net5.0 | 9.5KB | 305.8KB | +296.3KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | -| net6.0 | 10.0KB | 224.7KB | +214.7KB | +17.7KB | +8.7KB | +1.1KB | +4.7KB | -| net7.0 | 10.0KB | 170.9KB | +160.9KB | +16.6KB | +6.9KB | +1.1KB | +4.2KB | -| net8.0 | 9.5KB | 127.6KB | +118.1KB | +16.0KB | +811bytes | +1.1KB | +4.2KB | +| netstandard2.0 | 8.0KB | 453.3KB | +445.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netstandard2.1 | 8.5KB | 381.2KB | +372.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net461 | 8.5KB | 452.9KB | +444.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net462 | 7.0KB | 456.4KB | +449.4KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net47 | 7.0KB | 456.1KB | +449.1KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net471 | 8.5KB | 454.8KB | +446.3KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| net472 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | +| net48 | 8.5KB | 452.2KB | +443.7KB | +16.7KB | +8.7KB | +14.4KB | +19.4KB | +| net481 | 8.5KB | 452.7KB | +444.2KB | +16.2KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp2.0 | 9.0KB | 419.5KB | +410.5KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | +| netcoreapp2.1 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | +| netcoreapp2.2 | 9.0KB | 387.9KB | +378.9KB | +16.7KB | +8.7KB | +13.9KB | +19.4KB | +| netcoreapp3.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 368.1KB | +358.6KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 313.0KB | +303.5KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | +| net6.0 | 10.0KB | 231.9KB | +221.9KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | +| net7.0 | 10.0KB | 178.3KB | +168.3KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB | +| net8.0 | 9.5KB | 135.4KB | +125.9KB | +16.0KB | +299bytes | +1.1KB | +4.2KB | | net9.0 | 9.5KB | 68.6KB | +59.1KB | +16.5KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 36.5KB | +26.5KB | +16.5KB | | +1.1KB | +4.2KB | | net11.0 | 10.0KB | 27.4KB | +17.4KB | +16.5KB | | +1.1KB | +4.2KB | @@ -753,8 +753,16 @@ The class `Polyfill` includes the following extension methods: #### Dictionary * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0) + * Note: No-op on older targets; the BCL grows the backing storage. + * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). + * Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. * `void TrimExcess()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess) + * Note: No-op on older targets; the BCL shrinks the backing storage. + * `bool TryGetAlternateLookup(DictionaryAlternateLookup) where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). #### DictionaryEntry @@ -929,7 +937,14 @@ The class `Polyfill` includes the following extension methods: #### HashSet * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. + * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). + * Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. + * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0) + * Note: Lookups are O(n) on older targets; the BCL is O(1). * `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0) @@ -1129,8 +1144,11 @@ The class `Polyfill` includes the following extension methods: * `void AddRange(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.addrange?view=net-11.0) * `void CopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.copyto?view=net-11.0) * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. + * Note: Returns void on older targets; the BCL returns int (the new capacity). * `void InsertRange(int, ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.collectionextensions.insertrange?view=net-11.0) * `void TrimExcess()` + * Note: No-op on older targets; the BCL shrinks the backing storage. #### Math @@ -1269,7 +1287,9 @@ The class `Polyfill` includes the following extension methods: #### Queue * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32)) + * Note: No-op on older targets; the BCL grows the backing storage. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. #### Random @@ -1519,7 +1539,9 @@ The class `Polyfill` includes the following extension methods: #### Stack * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0) + * Note: No-op on older targets; the BCL grows the backing storage. * `void TrimExcess(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32)) + * Note: No-op on older targets; the BCL shrinks the backing storage. * `bool TryPeek(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypeek?view=net-11.0) * `bool TryPop(T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trypop?view=net-11.0) @@ -2098,7 +2120,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2117,7 +2139,7 @@ void EnsureExample(Order order, Customer customer, string customerId, string ema this.quantity = Ensure.NotNegativeOrZero(quantity); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs index b05272a1..c5510dc2 100644 --- a/src/ApiBuilderTests/BuildApiTest.cs +++ b/src/ApiBuilderTests/BuildApiTest.cs @@ -158,6 +158,7 @@ static int WriteExtensions(StreamWriter writer, int count) .ToList(); writer.WriteLine($"#### {name}"); writer.WriteLine(); + WriteSectionGate(name, writer); if (instanceMethodsForType.Count != 0) { foreach (var method in instanceMethodsForType.OrderBy(Key)) @@ -268,11 +269,9 @@ static List ReadPropertiesForFiles(IEnumerable files) static void WriteType(string name, StreamWriter writer, ref int count) { - writer.WriteLine( - $""" - #### {name} - - """); + writer.WriteLine($"#### {name}"); + writer.WriteLine(); + WriteSectionGate(name, writer); count++; } @@ -283,6 +282,7 @@ static void WriteTypeMethods(string name, StreamWriter writer, ref int count, IE { writer.WriteLine($"#### {name}"); writer.WriteLine(); + WriteSectionGate(name, writer); foreach (var method in methods.OrderBy(Key)) { count++; @@ -293,6 +293,30 @@ static void WriteTypeMethods(string name, StreamWriter writer, ref int count, IE writer.WriteLine(); } + // Sections whose every member requires an MSBuild opt-in flag. Listed here (not + // derived from path) because some directories with opt-ins also contribute methods + // to mixed sections like StringBuilder; only fully-gated section names belong here. + // The anchor points to the readme heading that explains the flag. + static readonly Dictionary sectionGates = new() + { + ["ArgumentException"] = ("PolyArgumentExceptions", "argumentexception-1"), + ["ArgumentNullException"] = ("PolyArgumentExceptions", "argumentexception-1"), + ["ArgumentOutOfRangeException"] = ("PolyArgumentExceptions", "argumentexception-1"), + ["ObjectDisposedException"] = ("PolyArgumentExceptions", "argumentexception-1"), + ["Ensure"] = ("PolyEnsure", "ensure-1"), + }; + + static void WriteSectionGate(string sectionName, StreamWriter writer) + { + if (!sectionGates.TryGetValue(sectionName, out var gate)) + { + return; + } + + writer.WriteLine($"> Requires [`<{gate.Flag}>true`](#{gate.Anchor}) in the consuming project."); + writer.WriteLine(); + } + static void WriteSignature(Method method, StreamWriter writer) { var signature = new StringBuilder($"{method.ReturnType} {method.Identifier.Text}{BuildTypeArgs(method)}({BuildParameters(method, true)})"); @@ -314,6 +338,8 @@ static void WriteSignature(Method method, StreamWriter writer) { writer.WriteLine($" * `{signature}`"); } + + WriteNotes(method, writer); } static void WriteSignature(Property method, StreamWriter writer) @@ -328,6 +354,16 @@ static void WriteSignature(Property method, StreamWriter writer) { writer.WriteLine($" * `{signature}`"); } + + WriteNotes(method, writer); + } + + static void WriteNotes(Member member, StreamWriter writer) + { + foreach (var note in member.GetNotes()) + { + writer.WriteLine($" * Note: {note}"); + } } static string Key(Method method) => diff --git a/src/ApiBuilderTests/LinkReader.cs b/src/ApiBuilderTests/LinkReader.cs index cd6b138f..682a6719 100644 --- a/src/ApiBuilderTests/LinkReader.cs +++ b/src/ApiBuilderTests/LinkReader.cs @@ -27,4 +27,27 @@ public static bool TryGetReference(this Member member, [NotNullWhen(true)] out s reference = null; return false; } + + public static IReadOnlyList GetNotes(this Member member) + { + List? notes = null; + foreach (var trivia in member.GetLeadingTrivia()) + { + if (!trivia.IsKind(SyntaxKind.SingleLineCommentTrivia)) + { + continue; + } + + var comment = trivia.ToString(); + if (!comment.StartsWith("//Note: ")) + { + continue; + } + + notes ??= []; + notes.Add(comment.Substring("//Note: ".Length)); + } + + return (IReadOnlyList?)notes ?? []; + } } diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index 353fb8c2..d52c1d9a 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -583,6 +583,44 @@ void Dictionary_Methods() pairs.ToDictionary(StringComparer.Ordinal); } +#if !NET9_0_OR_GREATER + void Dictionary_AlternateLookup() + { + var dictionary = new Dictionary(new KeyHolderAlternateComparer()); + var lookup = dictionary.GetAlternateLookup(); + _ = lookup.Dictionary; + lookup.ContainsKey("a"); + lookup.TryAdd("a", 1); + lookup.TryGetValue("a", out var value); + lookup.TryGetValue("a", out var actualKey, out value); + lookup["a"] = 2; + _ = lookup["a"]; + lookup.Remove("a"); + lookup.Remove("a", out actualKey, out value); + + if (dictionary.TryGetAlternateLookup(out var maybeLookup)) + { + _ = maybeLookup.Dictionary; + } + } + + class KeyHolder + { + public string Name { get; set; } = ""; + } + + class KeyHolderAlternateComparer : + IEqualityComparer, + IAlternateEqualityComparer + { + public bool Equals(KeyHolder? x, KeyHolder? y) => x?.Name == y?.Name; + public int GetHashCode(KeyHolder target) => target.Name.GetHashCode(); + public bool Equals(string alternate, KeyHolder other) => alternate == other.Name; + public int GetHashCode(string alternate) => alternate.GetHashCode(); + public KeyHolder Create(string alternate) => new() { Name = alternate }; + } +#endif + void Lock_Methods() { var locker = new Lock(); @@ -837,6 +875,24 @@ void HashSet_Methods() set.TrimExcess(); } +#if !NET9_0_OR_GREATER + void HashSet_AlternateLookup() + { + var set = new HashSet(new KeyHolderAlternateComparer()); + var lookup = set.GetAlternateLookup(); + _ = lookup.Set; + lookup.Add("a"); + lookup.Contains("a"); + lookup.TryGetValue("a", out var actual); + lookup.Remove("a"); + + if (set.TryGetAlternateLookup(out var maybeLookup)) + { + _ = maybeLookup.Set; + } + } +#endif + #if FeatureHttp void HttpClient_Methods(HttpClient target) { diff --git a/src/Polyfill/DictionaryAlternateLookup.cs b/src/Polyfill/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..8c1f78a8 --- /dev/null +++ b/src/Polyfill/DictionaryAlternateLookup.cs @@ -0,0 +1,158 @@ +#if !NET9_0_OR_GREATER + +namespace Polyfills; + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.alternatelookup-1?view=net-11.0 +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + + dictionary.Add(comparer.Create(key), value); + return true; + } + + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + + value = default; + return false; + } + + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + + value = default; + return false; + } + + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + + return false; + } + + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + + value = default; + return false; + } + + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + + actualKey = default; + return false; + } +} + +#endif diff --git a/src/Polyfill/HashSetAlternateLookup.cs b/src/Polyfill/HashSetAlternateLookup.cs new file mode 100644 index 00000000..56b48812 --- /dev/null +++ b/src/Polyfill/HashSetAlternateLookup.cs @@ -0,0 +1,90 @@ +#if !NET9_0_OR_GREATER + +namespace Polyfills; + +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.alternatelookup-1?view=net-11.0 +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + + /// Gets the against which this instance performs operations. + public HashSet Set => set; + + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + + set.Add(comparer.Create(item)); + return true; + } + + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + + return false; + } + + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + + actualValue = default; + return false; + } +} + +#endif diff --git a/src/Polyfill/IAlternateEqualityComparer.cs b/src/Polyfill/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..e82f7ca7 --- /dev/null +++ b/src/Polyfill/IAlternateEqualityComparer.cs @@ -0,0 +1,38 @@ +#if !NET9_0_OR_GREATER + +namespace System.Collections.Generic; + +using Diagnostics.CodeAnalysis; + +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +//Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.ialternateequalitycomparer-2?view=net-11.0 +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} + +#else +[assembly: System.Runtime.CompilerServices.TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))] +#endif diff --git a/src/Polyfill/Polyfill_Dictionary.cs b/src/Polyfill/Polyfill_Dictionary.cs index c5c33977..26d3b569 100644 --- a/src/Polyfill/Polyfill_Dictionary.cs +++ b/src/Polyfill/Polyfill_Dictionary.cs @@ -1,5 +1,6 @@ namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -57,6 +58,7 @@ public static bool Remove( /// Ensures that the capacity of this dictionary is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0 + //Note: No-op on older targets; the BCL grows the backing storage. public static void EnsureCapacity(this Dictionary target, int capacity) { } @@ -65,6 +67,7 @@ public static void EnsureCapacity(this Dictionary ta /// Sets the capacity of this dictionary to hold up a specified number of entries without any further expansion of its backing storage. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess(system-int32) + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this Dictionary target, int capacity) { } @@ -73,9 +76,55 @@ public static void TrimExcess(this Dictionary target /// Sets the capacity of this dictionary to what it would be if it had been originally initialized with all its entries. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trimexcess?view=net-11.0#system-collections-generic-dictionary-2-trimexcess + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this Dictionary target) { } +#endif + +#if !NET9_0_OR_GREATER + + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0 + //Note: Lookups are O(n) on older targets; the BCL is O(1). + //Note: Returns the free-standing `DictionaryAlternateLookup` rather than the BCL's nested `Dictionary.AlternateLookup`. Use `var` for cross-target code. + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + + return new(target, comparer); + } + + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.trygetalternatelookup?view=net-11.0 + //Note: Lookups are O(n) on older targets; the BCL is O(1). + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + + lookup = default; + return false; + } + #endif } diff --git a/src/Polyfill/Polyfill_HashSet.cs b/src/Polyfill/Polyfill_HashSet.cs index dd5dbcde..25ea53d1 100644 --- a/src/Polyfill/Polyfill_HashSet.cs +++ b/src/Polyfill/Polyfill_HashSet.cs @@ -1,5 +1,6 @@ namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -38,6 +39,7 @@ public static bool TryGetValue( /// Ensures that the capacity of this HashSet is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.ensurecapacity?view=net-11.0#system-collections-generic-hashset-1-ensurecapacity(system-int32) + //Note: No-op on older targets; the BCL grows the backing storage. public static void EnsureCapacity(this HashSet target, int capacity) { } @@ -50,9 +52,49 @@ public static void EnsureCapacity(this HashSet target, int capacity) /// Sets the capacity of a HashSet object to the specified number of entries, rounded up to a nearby, implementation-specific value. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trimexcess?view=net-11.0#system-collections-generic-hashset-1-trimexcess(system-int32) + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0 + //Note: Lookups are O(n) on older targets; the BCL is O(1). + //Note: Returns the free-standing `HashSetAlternateLookup` rather than the BCL's nested `HashSet.AlternateLookup`. Use `var` for cross-target code. + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + + return new(target, comparer); + } + + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0 + //Note: Lookups are O(n) on older targets; the BCL is O(1). + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + + lookup = default; + return false; + } + #endif } diff --git a/src/Polyfill/Polyfill_List.cs b/src/Polyfill/Polyfill_List.cs index fe9a65b8..32cbd23b 100644 --- a/src/Polyfill/Polyfill_List.cs +++ b/src/Polyfill/Polyfill_List.cs @@ -54,6 +54,8 @@ public static void CopyTo(this List target, Span destination) /// Ensures that the capacity of this list is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.ensurecapacity?view=net-11.0#system-collections-generic-list-1-ensurecapacity(system-int32) + //Note: No-op on older targets; the BCL grows the backing storage. + //Note: Returns void on older targets; the BCL returns int (the new capacity). public static void EnsureCapacity(this List target, int capacity) { } @@ -62,6 +64,7 @@ public static void EnsureCapacity(this List target, int capacity) /// Sets the capacity to the actual number of elements in the , if that number is less than a threshold value. /// //Link:https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.list-1.trimexcess?view=net-11.0 + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this List target) { } diff --git a/src/Polyfill/Polyfill_Queue.cs b/src/Polyfill/Polyfill_Queue.cs index 0bb23e74..c288a1df 100644 --- a/src/Polyfill/Polyfill_Queue.cs +++ b/src/Polyfill/Polyfill_Queue.cs @@ -10,6 +10,7 @@ static partial class Polyfill /// Ensures that the capacity of this queue is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.ensurecapacity?view=net-11.0#system-collections-generic-queue-1-ensurecapacity(system-int32) + //Note: No-op on older targets; the BCL grows the backing storage. public static void EnsureCapacity(this Queue target, int capacity) { } @@ -22,6 +23,7 @@ public static void EnsureCapacity(this Queue target, int capacity) /// Sets the capacity of a Queue object to the specified number of entries. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.queue-1.trimexcess?view=net-11.0#system-collections-generic-queue-1-trimexcess(system-int32) + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this Queue target, int capacity) { } diff --git a/src/Polyfill/Polyfill_Stack.cs b/src/Polyfill/Polyfill_Stack.cs index dc5aa602..3f63a955 100644 --- a/src/Polyfill/Polyfill_Stack.cs +++ b/src/Polyfill/Polyfill_Stack.cs @@ -11,6 +11,7 @@ static partial class Polyfill /// Ensures that the capacity of this Stack is at least the specified capacity. If the current capacity is less than capacity, it is increased to at least the specified capacity. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.ensurecapacity?view=net-11.0 + //Note: No-op on older targets; the BCL grows the backing storage. public static void EnsureCapacity(this Stack target, int capacity) { } @@ -23,6 +24,7 @@ public static void EnsureCapacity(this Stack target, int capacity) /// Sets the capacity of a Stack object to a specified number of entries. /// //Link: https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.stack-1.trimexcess?view=net-11.0#system-collections-generic-stack-1-trimexcess(system-int32) + //Note: No-op on older targets; the BCL shrinks the backing storage. public static void TrimExcess(this Stack target, int capacity) { } diff --git a/src/Split/net10.0/TypeForwardeds.cs b/src/Split/net10.0/TypeForwardeds.cs index 1244a4bb..b17d055b 100644 --- a/src/Split/net10.0/TypeForwardeds.cs +++ b/src/Split/net10.0/TypeForwardeds.cs @@ -13,6 +13,7 @@ [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureGuardAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] [assembly: TypeForwardedTo(typeof(System.Collections.Generic.KeyValuePair))] [assembly: TypeForwardedTo(typeof(System.Threading.Lock))] diff --git a/src/Split/net11.0/TypeForwardeds.cs b/src/Split/net11.0/TypeForwardeds.cs index 1244a4bb..b17d055b 100644 --- a/src/Split/net11.0/TypeForwardeds.cs +++ b/src/Split/net11.0/TypeForwardeds.cs @@ -13,6 +13,7 @@ [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureGuardAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] [assembly: TypeForwardedTo(typeof(System.Collections.Generic.KeyValuePair))] [assembly: TypeForwardedTo(typeof(System.Threading.Lock))] diff --git a/src/Split/net461/DictionaryAlternateLookup.cs b/src/Split/net461/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net461/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net461/HashSetAlternateLookup.cs b/src/Split/net461/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net461/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net461/IAlternateEqualityComparer.cs b/src/Split/net461/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net461/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net461/Polyfill_Dictionary.cs b/src/Split/net461/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net461/Polyfill_Dictionary.cs +++ b/src/Split/net461/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net461/Polyfill_HashSet.cs b/src/Split/net461/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/net461/Polyfill_HashSet.cs +++ b/src/Split/net461/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net462/DictionaryAlternateLookup.cs b/src/Split/net462/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net462/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net462/HashSetAlternateLookup.cs b/src/Split/net462/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net462/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net462/IAlternateEqualityComparer.cs b/src/Split/net462/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net462/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net462/Polyfill_Dictionary.cs b/src/Split/net462/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net462/Polyfill_Dictionary.cs +++ b/src/Split/net462/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net462/Polyfill_HashSet.cs b/src/Split/net462/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/net462/Polyfill_HashSet.cs +++ b/src/Split/net462/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net47/DictionaryAlternateLookup.cs b/src/Split/net47/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net47/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net47/HashSetAlternateLookup.cs b/src/Split/net47/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net47/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net47/IAlternateEqualityComparer.cs b/src/Split/net47/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net47/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net47/Polyfill_Dictionary.cs b/src/Split/net47/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net47/Polyfill_Dictionary.cs +++ b/src/Split/net47/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net47/Polyfill_HashSet.cs b/src/Split/net47/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/net47/Polyfill_HashSet.cs +++ b/src/Split/net47/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net471/DictionaryAlternateLookup.cs b/src/Split/net471/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net471/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net471/HashSetAlternateLookup.cs b/src/Split/net471/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net471/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net471/IAlternateEqualityComparer.cs b/src/Split/net471/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net471/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net471/Polyfill_Dictionary.cs b/src/Split/net471/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net471/Polyfill_Dictionary.cs +++ b/src/Split/net471/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net471/Polyfill_HashSet.cs b/src/Split/net471/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/net471/Polyfill_HashSet.cs +++ b/src/Split/net471/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net472/DictionaryAlternateLookup.cs b/src/Split/net472/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net472/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net472/HashSetAlternateLookup.cs b/src/Split/net472/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net472/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net472/IAlternateEqualityComparer.cs b/src/Split/net472/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net472/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net472/Polyfill_Dictionary.cs b/src/Split/net472/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net472/Polyfill_Dictionary.cs +++ b/src/Split/net472/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net472/Polyfill_HashSet.cs b/src/Split/net472/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/net472/Polyfill_HashSet.cs +++ b/src/Split/net472/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net48/DictionaryAlternateLookup.cs b/src/Split/net48/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net48/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net48/HashSetAlternateLookup.cs b/src/Split/net48/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net48/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net48/IAlternateEqualityComparer.cs b/src/Split/net48/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net48/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net48/Polyfill_Dictionary.cs b/src/Split/net48/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net48/Polyfill_Dictionary.cs +++ b/src/Split/net48/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net48/Polyfill_HashSet.cs b/src/Split/net48/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/net48/Polyfill_HashSet.cs +++ b/src/Split/net48/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net481/DictionaryAlternateLookup.cs b/src/Split/net481/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net481/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net481/HashSetAlternateLookup.cs b/src/Split/net481/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net481/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net481/IAlternateEqualityComparer.cs b/src/Split/net481/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net481/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net481/Polyfill_Dictionary.cs b/src/Split/net481/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/net481/Polyfill_Dictionary.cs +++ b/src/Split/net481/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net481/Polyfill_HashSet.cs b/src/Split/net481/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/net481/Polyfill_HashSet.cs +++ b/src/Split/net481/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net5.0/DictionaryAlternateLookup.cs b/src/Split/net5.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net5.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net5.0/HashSetAlternateLookup.cs b/src/Split/net5.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net5.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net5.0/IAlternateEqualityComparer.cs b/src/Split/net5.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net5.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net5.0/Polyfill_Dictionary.cs b/src/Split/net5.0/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/net5.0/Polyfill_Dictionary.cs +++ b/src/Split/net5.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net5.0/Polyfill_HashSet.cs b/src/Split/net5.0/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/net5.0/Polyfill_HashSet.cs +++ b/src/Split/net5.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net6.0/DictionaryAlternateLookup.cs b/src/Split/net6.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net6.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net6.0/HashSetAlternateLookup.cs b/src/Split/net6.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net6.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net6.0/IAlternateEqualityComparer.cs b/src/Split/net6.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net6.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net6.0/Polyfill_Dictionary.cs b/src/Split/net6.0/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/net6.0/Polyfill_Dictionary.cs +++ b/src/Split/net6.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net6.0/Polyfill_HashSet.cs b/src/Split/net6.0/Polyfill_HashSet.cs index 571f1618..fa166417 100644 --- a/src/Split/net6.0/Polyfill_HashSet.cs +++ b/src/Split/net6.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -11,4 +12,34 @@ static partial class Polyfill public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net7.0/DictionaryAlternateLookup.cs b/src/Split/net7.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net7.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net7.0/HashSetAlternateLookup.cs b/src/Split/net7.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net7.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net7.0/IAlternateEqualityComparer.cs b/src/Split/net7.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net7.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net7.0/Polyfill_Dictionary.cs b/src/Split/net7.0/Polyfill_Dictionary.cs new file mode 100644 index 00000000..32cb6b54 --- /dev/null +++ b/src/Split/net7.0/Polyfill_Dictionary.cs @@ -0,0 +1,42 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +static partial class Polyfill +{ + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } +} diff --git a/src/Split/net7.0/Polyfill_HashSet.cs b/src/Split/net7.0/Polyfill_HashSet.cs index 571f1618..fa166417 100644 --- a/src/Split/net7.0/Polyfill_HashSet.cs +++ b/src/Split/net7.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -11,4 +12,34 @@ static partial class Polyfill public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net8.0/DictionaryAlternateLookup.cs b/src/Split/net8.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/net8.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/net8.0/HashSetAlternateLookup.cs b/src/Split/net8.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/net8.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/net8.0/IAlternateEqualityComparer.cs b/src/Split/net8.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/net8.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/net8.0/Polyfill_Dictionary.cs b/src/Split/net8.0/Polyfill_Dictionary.cs new file mode 100644 index 00000000..32cb6b54 --- /dev/null +++ b/src/Split/net8.0/Polyfill_Dictionary.cs @@ -0,0 +1,42 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +static partial class Polyfill +{ + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } +} diff --git a/src/Split/net8.0/Polyfill_HashSet.cs b/src/Split/net8.0/Polyfill_HashSet.cs index 571f1618..fa166417 100644 --- a/src/Split/net8.0/Polyfill_HashSet.cs +++ b/src/Split/net8.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -11,4 +12,34 @@ static partial class Polyfill public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/net9.0/TypeForwardeds.cs b/src/Split/net9.0/TypeForwardeds.cs index 1244a4bb..b17d055b 100644 --- a/src/Split/net9.0/TypeForwardeds.cs +++ b/src/Split/net9.0/TypeForwardeds.cs @@ -13,6 +13,7 @@ [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.ExperimentalAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureGuardAttribute))] [assembly: TypeForwardedTo(typeof(System.Diagnostics.CodeAnalysis.FeatureSwitchDefinitionAttribute))] +[assembly: TypeForwardedTo(typeof(System.Collections.Generic.IAlternateEqualityComparer<,>))] [assembly: TypeForwardedTo(typeof(System.Runtime.CompilerServices.IsExternalInit))] [assembly: TypeForwardedTo(typeof(System.Collections.Generic.KeyValuePair))] [assembly: TypeForwardedTo(typeof(System.Threading.Lock))] diff --git a/src/Split/netcoreapp2.0/DictionaryAlternateLookup.cs b/src/Split/netcoreapp2.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netcoreapp2.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.0/HashSetAlternateLookup.cs b/src/Split/netcoreapp2.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netcoreapp2.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.0/IAlternateEqualityComparer.cs b/src/Split/netcoreapp2.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netcoreapp2.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netcoreapp2.0/Polyfill_Dictionary.cs b/src/Split/netcoreapp2.0/Polyfill_Dictionary.cs index 28098e0c..40c583a4 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Dictionary.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -30,4 +31,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp2.0/Polyfill_HashSet.cs b/src/Split/netcoreapp2.0/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netcoreapp2.0/Polyfill_HashSet.cs +++ b/src/Split/netcoreapp2.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp2.1/DictionaryAlternateLookup.cs b/src/Split/netcoreapp2.1/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netcoreapp2.1/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.1/HashSetAlternateLookup.cs b/src/Split/netcoreapp2.1/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netcoreapp2.1/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.1/IAlternateEqualityComparer.cs b/src/Split/netcoreapp2.1/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netcoreapp2.1/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netcoreapp2.1/Polyfill_Dictionary.cs b/src/Split/netcoreapp2.1/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/netcoreapp2.1/Polyfill_Dictionary.cs +++ b/src/Split/netcoreapp2.1/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp2.1/Polyfill_HashSet.cs b/src/Split/netcoreapp2.1/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netcoreapp2.1/Polyfill_HashSet.cs +++ b/src/Split/netcoreapp2.1/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp2.2/DictionaryAlternateLookup.cs b/src/Split/netcoreapp2.2/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netcoreapp2.2/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.2/HashSetAlternateLookup.cs b/src/Split/netcoreapp2.2/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netcoreapp2.2/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netcoreapp2.2/IAlternateEqualityComparer.cs b/src/Split/netcoreapp2.2/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netcoreapp2.2/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netcoreapp2.2/Polyfill_Dictionary.cs b/src/Split/netcoreapp2.2/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/netcoreapp2.2/Polyfill_Dictionary.cs +++ b/src/Split/netcoreapp2.2/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp2.2/Polyfill_HashSet.cs b/src/Split/netcoreapp2.2/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netcoreapp2.2/Polyfill_HashSet.cs +++ b/src/Split/netcoreapp2.2/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp3.0/DictionaryAlternateLookup.cs b/src/Split/netcoreapp3.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netcoreapp3.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netcoreapp3.0/HashSetAlternateLookup.cs b/src/Split/netcoreapp3.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netcoreapp3.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netcoreapp3.0/IAlternateEqualityComparer.cs b/src/Split/netcoreapp3.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netcoreapp3.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netcoreapp3.0/Polyfill_Dictionary.cs b/src/Split/netcoreapp3.0/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/netcoreapp3.0/Polyfill_Dictionary.cs +++ b/src/Split/netcoreapp3.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp3.0/Polyfill_HashSet.cs b/src/Split/netcoreapp3.0/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netcoreapp3.0/Polyfill_HashSet.cs +++ b/src/Split/netcoreapp3.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp3.1/DictionaryAlternateLookup.cs b/src/Split/netcoreapp3.1/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netcoreapp3.1/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netcoreapp3.1/HashSetAlternateLookup.cs b/src/Split/netcoreapp3.1/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netcoreapp3.1/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netcoreapp3.1/IAlternateEqualityComparer.cs b/src/Split/netcoreapp3.1/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netcoreapp3.1/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netcoreapp3.1/Polyfill_Dictionary.cs b/src/Split/netcoreapp3.1/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/netcoreapp3.1/Polyfill_Dictionary.cs +++ b/src/Split/netcoreapp3.1/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netcoreapp3.1/Polyfill_HashSet.cs b/src/Split/netcoreapp3.1/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netcoreapp3.1/Polyfill_HashSet.cs +++ b/src/Split/netcoreapp3.1/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netstandard2.0/DictionaryAlternateLookup.cs b/src/Split/netstandard2.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netstandard2.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netstandard2.0/HashSetAlternateLookup.cs b/src/Split/netstandard2.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netstandard2.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netstandard2.0/IAlternateEqualityComparer.cs b/src/Split/netstandard2.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netstandard2.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netstandard2.0/Polyfill_Dictionary.cs b/src/Split/netstandard2.0/Polyfill_Dictionary.cs index 0c560b6a..364b96cd 100644 --- a/src/Split/netstandard2.0/Polyfill_Dictionary.cs +++ b/src/Split/netstandard2.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -56,4 +57,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netstandard2.0/Polyfill_HashSet.cs b/src/Split/netstandard2.0/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/netstandard2.0/Polyfill_HashSet.cs +++ b/src/Split/netstandard2.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netstandard2.1/DictionaryAlternateLookup.cs b/src/Split/netstandard2.1/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/netstandard2.1/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/netstandard2.1/HashSetAlternateLookup.cs b/src/Split/netstandard2.1/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/netstandard2.1/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/netstandard2.1/IAlternateEqualityComparer.cs b/src/Split/netstandard2.1/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/netstandard2.1/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/netstandard2.1/Polyfill_Dictionary.cs b/src/Split/netstandard2.1/Polyfill_Dictionary.cs index ff7f0f39..5c819f73 100644 --- a/src/Split/netstandard2.1/Polyfill_Dictionary.cs +++ b/src/Split/netstandard2.1/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -12,4 +13,36 @@ static partial class Polyfill public static ReadOnlyDictionary AsReadOnly(this IDictionary target) where TKey : notnull => new(target); + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/netstandard2.1/Polyfill_HashSet.cs b/src/Split/netstandard2.1/Polyfill_HashSet.cs index a0614d44..5917842b 100644 --- a/src/Split/netstandard2.1/Polyfill_HashSet.cs +++ b/src/Split/netstandard2.1/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -17,4 +18,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/uap10.0/DictionaryAlternateLookup.cs b/src/Split/uap10.0/DictionaryAlternateLookup.cs new file mode 100644 index 00000000..7405eb10 --- /dev/null +++ b/src/Split/uap10.0/DictionaryAlternateLookup.cs @@ -0,0 +1,136 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a as a key instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the dictionary's keys and invoking +/// on each. The native .NET 9+ counterpart +/// is O(1) by using the dictionary's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct DictionaryAlternateLookup + where TKey : notnull +{ + readonly Dictionary dictionary; + readonly IAlternateEqualityComparer comparer; + internal DictionaryAlternateLookup( + Dictionary dictionary, + IAlternateEqualityComparer comparer) + { + this.dictionary = dictionary; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public Dictionary Dictionary => dictionary; + /// Gets or sets the value associated with the specified alternate key. + public TValue this[TAlternateKey key] + { + get + { + if (TryFindKey(key, out var actual)) + { + return dictionary[actual]; + } + throw new KeyNotFoundException(); + } + set + { + if (TryFindKey(key, out var actual)) + { + dictionary[actual] = value; + } + else + { + dictionary.Add(comparer.Create(key), value); + } + } + } + /// Determines whether the dictionary contains the specified alternate key. + public bool ContainsKey(TAlternateKey key) => TryFindKey(key, out _); + /// Attempts to add the specified alternate key and value to the dictionary. + public bool TryAdd(TAlternateKey key, TValue value) + { + if (TryFindKey(key, out _)) + { + return false; + } + dictionary.Add(comparer.Create(key), value); + return true; + } + /// Gets the value associated with the specified alternate key. + public bool TryGetValue(TAlternateKey key, [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out var actual)) + { + value = dictionary[actual]; + return true; + } + value = default; + return false; + } + /// Gets the value associated with the specified alternate key, along with the actual key stored in the dictionary. + public bool TryGetValue( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + return true; + } + value = default; + return false; + } + /// Removes the value with the specified alternate key from the dictionary. + public bool Remove(TAlternateKey key) + { + if (TryFindKey(key, out var actual)) + { + dictionary.Remove(actual); + return true; + } + return false; + } + /// Removes the value with the specified alternate key from the dictionary, copying the actual key and value to the out parameters. + public bool Remove( + TAlternateKey key, + [MaybeNullWhen(false)] out TKey actualKey, + [MaybeNullWhen(false)] out TValue value) + { + if (TryFindKey(key, out actualKey)) + { + value = dictionary[actualKey]; + dictionary.Remove(actualKey); + return true; + } + value = default; + return false; + } + bool TryFindKey(TAlternateKey alternate, [MaybeNullWhen(false)] out TKey actualKey) + { + foreach (var existing in dictionary.Keys) + { + if (comparer.Equals(alternate, existing)) + { + actualKey = existing; + return true; + } + } + actualKey = default; + return false; + } +} diff --git a/src/Split/uap10.0/HashSetAlternateLookup.cs b/src/Split/uap10.0/HashSetAlternateLookup.cs new file mode 100644 index 00000000..f63523f7 --- /dev/null +++ b/src/Split/uap10.0/HashSetAlternateLookup.cs @@ -0,0 +1,75 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +/// +/// Provides a type that may be used to perform operations on a +/// using a instead of a . +/// +/// +/// This polyfill performs O(n) lookups by linearly scanning the set and invoking +/// on each element. The native .NET 9+ +/// counterpart is O(1) by using the set's internal hash buckets. +/// +[ExcludeFromCodeCoverage] +[DebuggerNonUserCode] +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +readonly struct HashSetAlternateLookup +{ + readonly HashSet set; + readonly IAlternateEqualityComparer comparer; + internal HashSetAlternateLookup( + HashSet set, + IAlternateEqualityComparer comparer) + { + this.set = set; + this.comparer = comparer; + } + /// Gets the against which this instance performs operations. + public HashSet Set => set; + /// Adds an item that is created from the specified alternate value, if no equal value already exists. + public bool Add(TAlternate item) + { + if (TryFindValue(item, out _)) + { + return false; + } + set.Add(comparer.Create(item)); + return true; + } + /// Determines whether the set contains a value equal to the specified alternate value. + public bool Contains(TAlternate item) => TryFindValue(item, out _); + /// Removes the value equal to the specified alternate value from the set. + public bool Remove(TAlternate item) + { + if (TryFindValue(item, out var actual)) + { + set.Remove(actual); + return true; + } + return false; + } + /// Searches the set for the value equal to the specified alternate value and returns it. + public bool TryGetValue(TAlternate equalValue, [MaybeNullWhen(false)] out T actualValue) => + TryFindValue(equalValue, out actualValue); + bool TryFindValue(TAlternate alternate, [MaybeNullWhen(false)] out T actualValue) + { + foreach (var existing in set) + { + if (comparer.Equals(alternate, existing)) + { + actualValue = existing; + return true; + } + } + actualValue = default; + return false; + } +} diff --git a/src/Split/uap10.0/IAlternateEqualityComparer.cs b/src/Split/uap10.0/IAlternateEqualityComparer.cs new file mode 100644 index 00000000..9b3d7a09 --- /dev/null +++ b/src/Split/uap10.0/IAlternateEqualityComparer.cs @@ -0,0 +1,29 @@ +// +#pragma warning disable +namespace System.Collections.Generic; +using Diagnostics.CodeAnalysis; +/// +/// Implemented by an to support comparing +/// a instance with a instance. +/// +#if PolyUseEmbeddedAttribute +[global::Microsoft.CodeAnalysis.EmbeddedAttribute] +#endif +#if PolyPublic +public +#endif +interface IAlternateEqualityComparer +{ + /// + /// Determines whether the specified equals the specified . + /// + bool Equals(TAlternate alternate, T other); + /// + /// Returns a hash code for the specified alternate instance. + /// + int GetHashCode(TAlternate alternate); + /// + /// Creates a that is considered equal to the specified . + /// + T Create(TAlternate alternate); +} diff --git a/src/Split/uap10.0/Polyfill_Dictionary.cs b/src/Split/uap10.0/Polyfill_Dictionary.cs index 28098e0c..40c583a4 100644 --- a/src/Split/uap10.0/Polyfill_Dictionary.cs +++ b/src/Split/uap10.0/Polyfill_Dictionary.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; @@ -30,4 +31,36 @@ public static void TrimExcess(this Dictionary target public static void TrimExcess(this Dictionary target) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static DictionaryAlternateLookup GetAlternateLookup( + this Dictionary target) + where TKey : notnull + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The dictionary's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternateKey)}, {typeof(TKey)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a as a key instead of a . + /// + public static bool TryGetAlternateLookup( + this Dictionary target, + out DictionaryAlternateLookup lookup) + where TKey : notnull + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Split/uap10.0/Polyfill_HashSet.cs b/src/Split/uap10.0/Polyfill_HashSet.cs index 4c37b22d..95f54884 100644 --- a/src/Split/uap10.0/Polyfill_HashSet.cs +++ b/src/Split/uap10.0/Polyfill_HashSet.cs @@ -1,6 +1,7 @@ // #pragma warning disable namespace Polyfills; +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; static partial class Polyfill @@ -39,4 +40,34 @@ public static void EnsureCapacity(this HashSet target, int capacity) public static void TrimExcess(this HashSet target, int capacity) { } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static HashSetAlternateLookup GetAlternateLookup( + this HashSet target) + { + if (target.Comparer is not IAlternateEqualityComparer comparer) + { + throw new InvalidOperationException( + $"The set's comparer ({target.Comparer.GetType()}) does not implement IAlternateEqualityComparer<{typeof(TAlternate)}, {typeof(T)}>."); + } + return new(target, comparer); + } + /// + /// Gets an instance of a type that may be used to perform operations on the current + /// using a instead of a . + /// + public static bool TryGetAlternateLookup( + this HashSet target, + out HashSetAlternateLookup lookup) + { + if (target.Comparer is IAlternateEqualityComparer comparer) + { + lookup = new(target, comparer); + return true; + } + lookup = default; + return false; + } } diff --git a/src/Tests/PolyfillTests_Dictionary.cs b/src/Tests/PolyfillTests_Dictionary.cs index 705c1543..ca2cb06d 100644 --- a/src/Tests/PolyfillTests_Dictionary.cs +++ b/src/Tests/PolyfillTests_Dictionary.cs @@ -178,4 +178,106 @@ public async Task Dictionary_TrimExcess() // Should not throw await Assert.That(dictionary.Count).IsEqualTo(2); } + + sealed class PersonByNameComparer : + IEqualityComparer, + IAlternateEqualityComparer + { + public static readonly PersonByNameComparer Instance = new(); + + public bool Equals(Person? left, Person? right) => + string.Equals(left?.Name, right?.Name, StringComparison.Ordinal); + + public int GetHashCode(Person person) => person.Name.GetHashCode(); + + public bool Equals(string alternate, Person other) => + string.Equals(alternate, other.Name, StringComparison.Ordinal); + + public int GetHashCode(string alternate) => alternate.GetHashCode(); + + public Person Create(string alternate) => new(alternate, 0); + } + + sealed record Person(string Name, int Age); + + [Test] + public async Task Dictionary_GetAlternateLookup() + { + var dictionary = new Dictionary(PersonByNameComparer.Instance) + { + [new("alice", 30)] = 1, + [new("bob", 40)] = 2, + }; + +#if NET9_0_OR_GREATER + var lookup = dictionary.GetAlternateLookup(); +#else + var lookup = dictionary.GetAlternateLookup(); +#endif + + await Assert.That(lookup.ContainsKey("alice")).IsTrue(); + await Assert.That(lookup.ContainsKey("missing")).IsFalse(); + await Assert.That(lookup["bob"]).IsEqualTo(2); + + await Assert.That(lookup.TryGetValue("alice", out var value)).IsTrue(); + await Assert.That(value).IsEqualTo(1); + + await Assert.That(lookup.TryGetValue("alice", out var actualKey, out value)).IsTrue(); + await Assert.That(actualKey!.Age).IsEqualTo(30); + await Assert.That(value).IsEqualTo(1); + + await Assert.That(lookup.TryAdd("carol", 3)).IsTrue(); + await Assert.That(lookup.TryAdd("carol", 99)).IsFalse(); + await Assert.That(lookup["carol"]).IsEqualTo(3); + + lookup["carol"] = 33; + await Assert.That(lookup["carol"]).IsEqualTo(33); + + await Assert.That(lookup.Remove("bob")).IsTrue(); + await Assert.That(lookup.ContainsKey("bob")).IsFalse(); + await Assert.That(lookup.Remove("ghost")).IsFalse(); + } + + [Test] + public async Task Dictionary_TryGetAlternateLookup_Succeeds() + { + var dictionary = new Dictionary(PersonByNameComparer.Instance); + +#if NET9_0_OR_GREATER + var found = dictionary.TryGetAlternateLookup(out _); +#else + var found = dictionary.TryGetAlternateLookup(out _); +#endif + + await Assert.That(found).IsTrue(); + } + + [Test] + public async Task Dictionary_TryGetAlternateLookup_FailsForIncompatibleComparer() + { + var dictionary = new Dictionary(); + +#if NET9_0_OR_GREATER + var found = dictionary.TryGetAlternateLookup(out _); +#else + var found = dictionary.TryGetAlternateLookup(out _); +#endif + + await Assert.That(found).IsFalse(); + } + + [Test] + public async Task Dictionary_GetAlternateLookup_ThrowsForIncompatibleComparer() + { + var dictionary = new Dictionary(); + + await Assert.That(() => + { +#if NET9_0_OR_GREATER + _ = dictionary.GetAlternateLookup(); +#else + _ = dictionary.GetAlternateLookup(); +#endif + }).Throws(); + } } \ No newline at end of file diff --git a/src/Tests/PolyfillTests_HashSet.cs b/src/Tests/PolyfillTests_HashSet.cs index 3e26830f..3fa94afb 100644 --- a/src/Tests/PolyfillTests_HashSet.cs +++ b/src/Tests/PolyfillTests_HashSet.cs @@ -33,4 +33,84 @@ public async Task HashSet_TrimExcess() // Should not throw await Assert.That(set.Count).IsEqualTo(3); } + + sealed class TagByNameComparer : + IEqualityComparer, + IAlternateEqualityComparer + { + public static readonly TagByNameComparer Instance = new(); + + public bool Equals(Tag? left, Tag? right) => + string.Equals(left?.Name, right?.Name, StringComparison.Ordinal); + + public int GetHashCode(Tag tag) => tag.Name.GetHashCode(); + + public bool Equals(string alternate, Tag other) => + string.Equals(alternate, other.Name, StringComparison.Ordinal); + + public int GetHashCode(string alternate) => alternate.GetHashCode(); + + public Tag Create(string alternate) => new(alternate); + } + + sealed record Tag(string Name); + + [Test] + public async Task HashSet_GetAlternateLookup() + { + var set = new HashSet(TagByNameComparer.Instance) + { + new("red"), + new("green"), + }; + +#if NET9_0_OR_GREATER + var lookup = set.GetAlternateLookup(); +#else + var lookup = set.GetAlternateLookup(); +#endif + + await Assert.That(lookup.Contains("red")).IsTrue(); + await Assert.That(lookup.Contains("blue")).IsFalse(); + + await Assert.That(lookup.TryGetValue("green", out var actual)).IsTrue(); + await Assert.That(actual!.Name).IsEqualTo("green"); + + await Assert.That(lookup.Add("blue")).IsTrue(); + await Assert.That(lookup.Add("blue")).IsFalse(); + await Assert.That(lookup.Contains("blue")).IsTrue(); + + await Assert.That(lookup.Remove("red")).IsTrue(); + await Assert.That(lookup.Contains("red")).IsFalse(); + await Assert.That(lookup.Remove("ghost")).IsFalse(); + } + + [Test] + public async Task HashSet_TryGetAlternateLookup_FailsForIncompatibleComparer() + { + var set = new HashSet(); + +#if NET9_0_OR_GREATER + var found = set.TryGetAlternateLookup(out _); +#else + var found = set.TryGetAlternateLookup(out _); +#endif + + await Assert.That(found).IsFalse(); + } + + [Test] + public async Task HashSet_GetAlternateLookup_ThrowsForIncompatibleComparer() + { + var set = new HashSet(); + + await Assert.That(() => + { +#if NET9_0_OR_GREATER + _ = set.GetAlternateLookup(); +#else + _ = set.GetAlternateLookup(); +#endif + }).Throws(); + } } \ No newline at end of file