From 76136788853e17b86c54db18668219eaa59837cb Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 10 May 2026 09:12:45 +1000 Subject: [PATCH 1/6] Add Dictionary/HashSet GetAlternateLookup polyfill (O(n)) --- apiCount.include.md | 40 ++--- api_list.include.md | 4 + assemblySize.include.md | 72 ++++---- readme.md | 120 ++++++------- src/Consume/Consume.cs | 56 +++++++ src/Polyfill/DictionaryAlternateLookup.cs | 158 ++++++++++++++++++ src/Polyfill/HashSetAlternateLookup.cs | 90 ++++++++++ src/Polyfill/IAlternateEqualityComparer.cs | 38 +++++ src/Polyfill/Polyfill_Dictionary.cs | 43 +++++ src/Polyfill/Polyfill_HashSet.cs | 37 ++++ src/Split/net10.0/TypeForwardeds.cs | 1 + src/Split/net11.0/TypeForwardeds.cs | 1 + src/Split/net461/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net461/HashSetAlternateLookup.cs | 75 +++++++++ .../net461/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net461/Polyfill_Dictionary.cs | 33 ++++ src/Split/net461/Polyfill_HashSet.cs | 31 ++++ src/Split/net462/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net462/HashSetAlternateLookup.cs | 75 +++++++++ .../net462/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net462/Polyfill_Dictionary.cs | 33 ++++ src/Split/net462/Polyfill_HashSet.cs | 31 ++++ src/Split/net47/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net47/HashSetAlternateLookup.cs | 75 +++++++++ src/Split/net47/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net47/Polyfill_Dictionary.cs | 33 ++++ src/Split/net47/Polyfill_HashSet.cs | 31 ++++ src/Split/net471/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net471/HashSetAlternateLookup.cs | 75 +++++++++ .../net471/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net471/Polyfill_Dictionary.cs | 33 ++++ src/Split/net471/Polyfill_HashSet.cs | 31 ++++ src/Split/net472/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net472/HashSetAlternateLookup.cs | 75 +++++++++ .../net472/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net472/Polyfill_Dictionary.cs | 33 ++++ src/Split/net472/Polyfill_HashSet.cs | 31 ++++ src/Split/net48/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net48/HashSetAlternateLookup.cs | 75 +++++++++ src/Split/net48/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net48/Polyfill_Dictionary.cs | 33 ++++ src/Split/net48/Polyfill_HashSet.cs | 31 ++++ src/Split/net481/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net481/HashSetAlternateLookup.cs | 75 +++++++++ .../net481/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net481/Polyfill_Dictionary.cs | 33 ++++ src/Split/net481/Polyfill_HashSet.cs | 31 ++++ src/Split/net5.0/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net5.0/HashSetAlternateLookup.cs | 75 +++++++++ .../net5.0/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net5.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/net5.0/Polyfill_HashSet.cs | 31 ++++ src/Split/net6.0/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net6.0/HashSetAlternateLookup.cs | 75 +++++++++ .../net6.0/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net6.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/net6.0/Polyfill_HashSet.cs | 31 ++++ src/Split/net7.0/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net7.0/HashSetAlternateLookup.cs | 75 +++++++++ .../net7.0/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net7.0/Polyfill_Dictionary.cs | 42 +++++ src/Split/net7.0/Polyfill_HashSet.cs | 31 ++++ src/Split/net8.0/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/net8.0/HashSetAlternateLookup.cs | 75 +++++++++ .../net8.0/IAlternateEqualityComparer.cs | 29 ++++ src/Split/net8.0/Polyfill_Dictionary.cs | 42 +++++ src/Split/net8.0/Polyfill_HashSet.cs | 31 ++++ src/Split/net9.0/TypeForwardeds.cs | 1 + .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netcoreapp2.0/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netcoreapp2.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/netcoreapp2.0/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netcoreapp2.1/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netcoreapp2.1/Polyfill_Dictionary.cs | 33 ++++ src/Split/netcoreapp2.1/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netcoreapp2.2/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netcoreapp2.2/Polyfill_Dictionary.cs | 33 ++++ src/Split/netcoreapp2.2/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netcoreapp3.0/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netcoreapp3.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/netcoreapp3.0/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netcoreapp3.1/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netcoreapp3.1/Polyfill_Dictionary.cs | 33 ++++ src/Split/netcoreapp3.1/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netstandard2.0/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netstandard2.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/netstandard2.0/Polyfill_HashSet.cs | 31 ++++ .../DictionaryAlternateLookup.cs | 136 +++++++++++++++ .../netstandard2.1/HashSetAlternateLookup.cs | 75 +++++++++ .../IAlternateEqualityComparer.cs | 29 ++++ .../netstandard2.1/Polyfill_Dictionary.cs | 33 ++++ src/Split/netstandard2.1/Polyfill_HashSet.cs | 31 ++++ .../uap10.0/DictionaryAlternateLookup.cs | 136 +++++++++++++++ src/Split/uap10.0/HashSetAlternateLookup.cs | 75 +++++++++ .../uap10.0/IAlternateEqualityComparer.cs | 29 ++++ src/Split/uap10.0/Polyfill_Dictionary.cs | 33 ++++ src/Split/uap10.0/Polyfill_HashSet.cs | 31 ++++ src/Tests/PolyfillTests_Dictionary.cs | 102 +++++++++++ src/Tests/PolyfillTests_HashSet.cs | 80 +++++++++ 110 files changed, 6523 insertions(+), 114 deletions(-) create mode 100644 src/Polyfill/DictionaryAlternateLookup.cs create mode 100644 src/Polyfill/HashSetAlternateLookup.cs create mode 100644 src/Polyfill/IAlternateEqualityComparer.cs create mode 100644 src/Split/net461/DictionaryAlternateLookup.cs create mode 100644 src/Split/net461/HashSetAlternateLookup.cs create mode 100644 src/Split/net461/IAlternateEqualityComparer.cs create mode 100644 src/Split/net462/DictionaryAlternateLookup.cs create mode 100644 src/Split/net462/HashSetAlternateLookup.cs create mode 100644 src/Split/net462/IAlternateEqualityComparer.cs create mode 100644 src/Split/net47/DictionaryAlternateLookup.cs create mode 100644 src/Split/net47/HashSetAlternateLookup.cs create mode 100644 src/Split/net47/IAlternateEqualityComparer.cs create mode 100644 src/Split/net471/DictionaryAlternateLookup.cs create mode 100644 src/Split/net471/HashSetAlternateLookup.cs create mode 100644 src/Split/net471/IAlternateEqualityComparer.cs create mode 100644 src/Split/net472/DictionaryAlternateLookup.cs create mode 100644 src/Split/net472/HashSetAlternateLookup.cs create mode 100644 src/Split/net472/IAlternateEqualityComparer.cs create mode 100644 src/Split/net48/DictionaryAlternateLookup.cs create mode 100644 src/Split/net48/HashSetAlternateLookup.cs create mode 100644 src/Split/net48/IAlternateEqualityComparer.cs create mode 100644 src/Split/net481/DictionaryAlternateLookup.cs create mode 100644 src/Split/net481/HashSetAlternateLookup.cs create mode 100644 src/Split/net481/IAlternateEqualityComparer.cs create mode 100644 src/Split/net5.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/net5.0/HashSetAlternateLookup.cs create mode 100644 src/Split/net5.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/net6.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/net6.0/HashSetAlternateLookup.cs create mode 100644 src/Split/net6.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/net7.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/net7.0/HashSetAlternateLookup.cs create mode 100644 src/Split/net7.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/net7.0/Polyfill_Dictionary.cs create mode 100644 src/Split/net8.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/net8.0/HashSetAlternateLookup.cs create mode 100644 src/Split/net8.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/net8.0/Polyfill_Dictionary.cs create mode 100644 src/Split/netcoreapp2.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.0/HashSetAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/netcoreapp2.1/DictionaryAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.1/HashSetAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.1/IAlternateEqualityComparer.cs create mode 100644 src/Split/netcoreapp2.2/DictionaryAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.2/HashSetAlternateLookup.cs create mode 100644 src/Split/netcoreapp2.2/IAlternateEqualityComparer.cs create mode 100644 src/Split/netcoreapp3.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/netcoreapp3.0/HashSetAlternateLookup.cs create mode 100644 src/Split/netcoreapp3.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/netcoreapp3.1/DictionaryAlternateLookup.cs create mode 100644 src/Split/netcoreapp3.1/HashSetAlternateLookup.cs create mode 100644 src/Split/netcoreapp3.1/IAlternateEqualityComparer.cs create mode 100644 src/Split/netstandard2.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/netstandard2.0/HashSetAlternateLookup.cs create mode 100644 src/Split/netstandard2.0/IAlternateEqualityComparer.cs create mode 100644 src/Split/netstandard2.1/DictionaryAlternateLookup.cs create mode 100644 src/Split/netstandard2.1/HashSetAlternateLookup.cs create mode 100644 src/Split/netstandard2.1/IAlternateEqualityComparer.cs create mode 100644 src/Split/uap10.0/DictionaryAlternateLookup.cs create mode 100644 src/Split/uap10.0/HashSetAlternateLookup.cs create mode 100644 src/Split/uap10.0/IAlternateEqualityComparer.cs diff --git a/apiCount.include.md b/apiCount.include.md index 99bea8deb..60540fc5f 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 cac42e886..55b973ff4 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -222,8 +222,10 @@ #### Dictionary * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0) + * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0) * `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)) * `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) + * `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) #### DictionaryEntry @@ -398,7 +400,9 @@ #### 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)) + * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0) * `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)) + * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0) * `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0) diff --git a/assemblySize.include.md b/assemblySize.include.md index 46c2f81ea..bb3a4875e 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.7KB | +446.2KB | +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.0KB | +360.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 368.0KB | +358.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 312.9KB | +303.4KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | +| net6.0 | 10.0KB | 231.8KB | +221.8KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | +| net7.0 | 10.0KB | 178.2KB | +168.2KB | +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/readme.md b/readme.md index a107e5f6a..62b98632f 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.7KB | +446.2KB | +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.0KB | +360.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| netcoreapp3.1 | 9.5KB | 368.0KB | +358.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | +| net5.0 | 9.5KB | 312.9KB | +303.4KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | +| net6.0 | 10.0KB | 231.8KB | +221.8KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | +| net7.0 | 10.0KB | 178.2KB | +168.2KB | +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,10 @@ 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) + * `DictionaryAlternateLookup GetAlternateLookup() where TKey : notnull` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.getalternatelookup?view=net-11.0) * `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)) * `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) + * `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) #### DictionaryEntry @@ -929,7 +931,9 @@ 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)) + * `HashSetAlternateLookup GetAlternateLookup()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.getalternatelookup?view=net-11.0) * `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)) + * `bool TryGetAlternateLookup(HashSetAlternateLookup)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetalternatelookup?view=net-11.0) * `bool TryGetValue(T, T)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.trygetvalue?view=net-11.0) @@ -2098,7 +2102,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2117,7 +2121,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/Consume/Consume.cs b/src/Consume/Consume.cs index 353fb8c21..d52c1d9ac 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 000000000..8c1f78a82 --- /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 000000000..56b488122 --- /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 000000000..e82f7ca7b --- /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 c5c33977e..7b8a17cf1 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; @@ -77,5 +78,47 @@ 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 + 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 + 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 dd5dbcdee..477890961 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; @@ -54,5 +55,41 @@ 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 + 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 + 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/Split/net10.0/TypeForwardeds.cs b/src/Split/net10.0/TypeForwardeds.cs index 1244a4bb1..b17d055bd 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 1244a4bb1..b17d055bd 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 4c37b22d6..95f54884d 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 4c37b22d6..95f54884d 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 4c37b22d6..95f54884d 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 4c37b22d6..95f54884d 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 571f16189..fa1664175 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 000000000..32cb6b545 --- /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 571f16189..fa1664175 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 000000000..32cb6b545 --- /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 571f16189..fa1664175 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 1244a4bb1..b17d055bd 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 28098e0c4..40c583a48 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 0c560b6ae..364b96cd2 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 4c37b22d6..95f54884d 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 ff7f0f392..5c819f732 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 a0614d44f..5917842bb 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 000000000..7405eb103 --- /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 000000000..f63523f71 --- /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 000000000..9b3d7a095 --- /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 28098e0c4..40c583a48 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 4c37b22d6..95f54884d 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 705c15431..ca2cb06d2 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 3e26830f1..3fa94afba 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 From bb8adea7410924826f5018f6366f059d362153d3 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 10 May 2026 09:17:43 +1000 Subject: [PATCH 2/6] . --- api_list.include.md | 6 ++++++ assemblySize.include.md | 12 ++++++------ claude.md | 1 + src/ApiBuilderTests/BuildApiTest.cs | 12 ++++++++++++ src/ApiBuilderTests/LinkReader.cs | 23 +++++++++++++++++++++++ src/Polyfill/Polyfill_Dictionary.cs | 3 +++ src/Polyfill/Polyfill_HashSet.cs | 3 +++ 7 files changed, 54 insertions(+), 6 deletions(-) diff --git a/api_list.include.md b/api_list.include.md index 55b973ff4..f0fa4b976 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -223,9 +223,12 @@ * `void EnsureCapacity(int)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.collections.generic.dictionary-2.ensurecapacity?view=net-11.0) * `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)) * `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) * `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 @@ -401,8 +404,11 @@ * `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)) * `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)) * `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) diff --git a/assemblySize.include.md b/assemblySize.include.md index bb3a4875e..186d75493 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -34,18 +34,18 @@ | 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.7KB | +446.2KB | +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.0KB | +360.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 368.0KB | +358.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 312.9KB | +303.4KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | -| net6.0 | 10.0KB | 231.8KB | +221.8KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | -| net7.0 | 10.0KB | 178.2KB | +168.2KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB | +| 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 | diff --git a/claude.md b/claude.md index aede57015..209a83ecf 100644 --- a/claude.md +++ b/claude.md @@ -69,6 +69,7 @@ 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. ### Test Projects diff --git a/src/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs index b05272a12..07b5b7999 100644 --- a/src/ApiBuilderTests/BuildApiTest.cs +++ b/src/ApiBuilderTests/BuildApiTest.cs @@ -314,6 +314,8 @@ static void WriteSignature(Method method, StreamWriter writer) { writer.WriteLine($" * `{signature}`"); } + + WriteNotes(method, writer); } static void WriteSignature(Property method, StreamWriter writer) @@ -328,6 +330,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 cd6b138f8..682a67193 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/Polyfill/Polyfill_Dictionary.cs b/src/Polyfill/Polyfill_Dictionary.cs index 7b8a17cf1..aca7c0f6e 100644 --- a/src/Polyfill/Polyfill_Dictionary.cs +++ b/src/Polyfill/Polyfill_Dictionary.cs @@ -87,6 +87,8 @@ public static void TrimExcess(this Dictionary target /// 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 @@ -105,6 +107,7 @@ public static DictionaryAlternateLookup GetAlternat /// 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) diff --git a/src/Polyfill/Polyfill_HashSet.cs b/src/Polyfill/Polyfill_HashSet.cs index 477890961..346ef7caa 100644 --- a/src/Polyfill/Polyfill_HashSet.cs +++ b/src/Polyfill/Polyfill_HashSet.cs @@ -60,6 +60,8 @@ public static void TrimExcess(this HashSet target, int capacity) /// 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) { @@ -77,6 +79,7 @@ public static HashSetAlternateLookup GetAlternateLookup 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) From 9c98a97f39eb2724c826ddf00d89a18063a2bb83 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 10 May 2026 09:33:43 +1000 Subject: [PATCH 3/6] . --- api_list.include.md | 12 ++++++++++++ readme.md | 30 +++++++++++++++++++++++------ src/Polyfill/Polyfill_Dictionary.cs | 3 +++ src/Polyfill/Polyfill_HashSet.cs | 2 ++ src/Polyfill/Polyfill_List.cs | 3 +++ src/Polyfill/Polyfill_Queue.cs | 2 ++ src/Polyfill/Polyfill_Stack.cs | 2 ++ 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/api_list.include.md b/api_list.include.md index f0fa4b976..628cd6173 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -222,11 +222,14 @@ #### 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). @@ -403,10 +406,12 @@ #### 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) @@ -608,8 +613,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 @@ -748,7 +756,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 @@ -998,7 +1008,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) diff --git a/readme.md b/readme.md index 62b98632f..2f9010268 100644 --- a/readme.md +++ b/readme.md @@ -128,18 +128,18 @@ This project uses features from the current stable SDK and C# language. As such | 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.7KB | +446.2KB | +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.0KB | +360.5KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | -| netcoreapp3.1 | 9.5KB | 368.0KB | +358.5KB | +16.7KB | +8.2KB | +14.4KB | +19.4KB | -| net5.0 | 9.5KB | 312.9KB | +303.4KB | +17.2KB | +8.7KB | +14.4KB | +19.9KB | -| net6.0 | 10.0KB | 231.8KB | +221.8KB | +20.2KB | +11.7KB | +1.6KB | +4.7KB | -| net7.0 | 10.0KB | 178.2KB | +168.2KB | +16.6KB | +6.9KB | +1.1KB | +4.7KB | +| 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 | @@ -753,10 +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 @@ -931,9 +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) @@ -1133,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 @@ -1273,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 @@ -1523,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) diff --git a/src/Polyfill/Polyfill_Dictionary.cs b/src/Polyfill/Polyfill_Dictionary.cs index aca7c0f6e..26d3b569b 100644 --- a/src/Polyfill/Polyfill_Dictionary.cs +++ b/src/Polyfill/Polyfill_Dictionary.cs @@ -58,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) { } @@ -66,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) { } @@ -74,6 +76,7 @@ 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) { } diff --git a/src/Polyfill/Polyfill_HashSet.cs b/src/Polyfill/Polyfill_HashSet.cs index 346ef7caa..25ea53d10 100644 --- a/src/Polyfill/Polyfill_HashSet.cs +++ b/src/Polyfill/Polyfill_HashSet.cs @@ -39,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) { } @@ -51,6 +52,7 @@ 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) { } diff --git a/src/Polyfill/Polyfill_List.cs b/src/Polyfill/Polyfill_List.cs index fe9a65b88..32cbd23b0 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 0bb23e746..c288a1df2 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 dc5aa6026..3f63a955e 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) { } From 14be79592ca2f2d2c9b4e3db257b1c8763c08144 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 10 May 2026 09:45:03 +1000 Subject: [PATCH 4/6] . --- api_list.include.md | 10 +++++++++ assemblySize.include.md | 4 ++-- claude.md | 1 + src/ApiBuilderTests/BuildApiTest.cs | 33 ++++++++++++++++++++++++----- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/api_list.include.md b/api_list.include.md index 628cd6173..50186e34c 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -2,18 +2,24 @@ #### ArgumentException +> Requires `true` 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` 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` 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)) @@ -665,6 +671,8 @@ #### ObjectDisposedException +> Requires `true` 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)) @@ -1330,6 +1338,8 @@ #### Ensure +> Requires `true` 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 186d75493..6c51da029 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -14,7 +14,7 @@ | 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.0 | 9.5KB | 257.5KB | +248.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | 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 | @@ -41,7 +41,7 @@ | 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.0 | 9.5KB | 370.1KB | +360.6KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | 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 | diff --git a/claude.md b/claude.md index 209a83ecf..8831aacc9 100644 --- a/claude.md +++ b/claude.md @@ -70,6 +70,7 @@ Polyfill uses extensive `#if` directives. Key constants: - 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/src/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs index 07b5b7999..a686baaad 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,29 @@ 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. + static readonly Dictionary sectionGates = new() + { + ["ArgumentException"] = "PolyArgumentExceptions", + ["ArgumentNullException"] = "PolyArgumentExceptions", + ["ArgumentOutOfRangeException"] = "PolyArgumentExceptions", + ["ObjectDisposedException"] = "PolyArgumentExceptions", + ["Ensure"] = "PolyEnsure", + }; + + static void WriteSectionGate(string sectionName, StreamWriter writer) + { + if (!sectionGates.TryGetValue(sectionName, out var flag)) + { + return; + } + + writer.WriteLine($"> Requires `<{flag}>true` 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)})"); From e6b527a22328bbe523ba283e0a09effd0dcf2e32 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sun, 10 May 2026 10:04:42 +1000 Subject: [PATCH 5/6] . --- api_list.include.md | 10 +++++----- assemblySize.include.md | 4 ++-- src/ApiBuilderTests/BuildApiTest.cs | 17 +++++++++-------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/api_list.include.md b/api_list.include.md index 50186e34c..9aa72d2e6 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -2,7 +2,7 @@ #### ArgumentException -> Requires `true` in the consuming project. +> 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)) @@ -10,7 +10,7 @@ #### ArgumentNullException -> Requires `true` in the consuming project. +> 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)) @@ -18,7 +18,7 @@ #### ArgumentOutOfRangeException -> Requires `true` in the consuming project. +> 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)` @@ -671,7 +671,7 @@ #### ObjectDisposedException -> Requires `true` in the consuming project. +> 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)) @@ -1338,7 +1338,7 @@ #### Ensure -> Requires `true` in the consuming project. +> Requires [`true`](#ensure-1) in the consuming project. * `void DirectoryExists(string)` * `T Equal(T, T)` diff --git a/assemblySize.include.md b/assemblySize.include.md index 6c51da029..186d75493 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -14,7 +14,7 @@ | 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 | +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 | @@ -41,7 +41,7 @@ | 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 | +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 | diff --git a/src/ApiBuilderTests/BuildApiTest.cs b/src/ApiBuilderTests/BuildApiTest.cs index a686baaad..c5510dc28 100644 --- a/src/ApiBuilderTests/BuildApiTest.cs +++ b/src/ApiBuilderTests/BuildApiTest.cs @@ -296,23 +296,24 @@ static void WriteTypeMethods(string name, StreamWriter writer, ref int count, IE // 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. - static readonly Dictionary sectionGates = new() + // The anchor points to the readme heading that explains the flag. + static readonly Dictionary sectionGates = new() { - ["ArgumentException"] = "PolyArgumentExceptions", - ["ArgumentNullException"] = "PolyArgumentExceptions", - ["ArgumentOutOfRangeException"] = "PolyArgumentExceptions", - ["ObjectDisposedException"] = "PolyArgumentExceptions", - ["Ensure"] = "PolyEnsure", + ["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 flag)) + if (!sectionGates.TryGetValue(sectionName, out var gate)) { return; } - writer.WriteLine($"> Requires `<{flag}>true` in the consuming project."); + writer.WriteLine($"> Requires [`<{gate.Flag}>true`](#{gate.Anchor}) in the consuming project."); writer.WriteLine(); } From 2644739cafce26e92b70469edacd4fbfab10e5d3 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Wed, 13 May 2026 07:44:25 +1000 Subject: [PATCH 6/6] refs or cleanup --- src/Directory.Packages.props | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 68ff37a79..4bf6124b7 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -6,9 +6,9 @@ - + - + @@ -18,7 +18,7 @@ - - + + \ No newline at end of file