Skip to content

Commit

Permalink
Added DictionaryExtensions.Merge function
Browse files Browse the repository at this point in the history
  • Loading branch information
ByronMayne committed Sep 26, 2023
1 parent 51e50a0 commit 36c4187
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 2 deletions.
63 changes: 63 additions & 0 deletions src/Extended.Collections.Tests/DictionaryExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
using Extended.Collections.Exceptions;
using FluentAssertions;
using Xunit;
using Xunit.Abstractions;

namespace Extended.Collections.Tests
{
public class DictionaryExtensionsTests : BaseTest
{

public DictionaryExtensionsTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper)
{ }

[Theory]
[InlineData(MergeMethod.KeepLast)]
[InlineData(MergeMethod.KeepFirst)]
public void Merge_Single_KeepsExpectedValue(MergeMethod mergeMethod)
{
Dictionary<string, MergeMethod> source = new() { ["Key"] = MergeMethod.KeepFirst };
Dictionary<string, MergeMethod> target = new() { ["Key"] = MergeMethod.KeepLast };

source.Merge(mergeMethod, target)
.Should()
.HaveCount(1)
.And.Contain("Key", mergeMethod);
}

[Theory]
[InlineData(MergeMethod.KeepLast)]
[InlineData(MergeMethod.KeepFirst)]
public void Merge_Multiple_KeepsExpectedValue(MergeMethod mergeMethod)
{
Dictionary<string, string> source = Create("Key", "First");
Dictionary<string, string>[] targets = new[] {
Create("Key", "Second"),
Create("Key", "Third"),
};

source.Merge(mergeMethod, targets)
.Should()
.HaveCount(1)
.And.Contain("Key", mergeMethod == MergeMethod.KeepFirst ? "First" : "Third");
}

[Fact]
public void Merge_WithDuplicateKeysAndThrowMethod_ThrowsDuplicateKeyException()
{
Dictionary<string, string> source = Create("key", "First");

Assert.Throws<DuplicateKeyException>(
() => source.Merge(MergeMethod.Throw, source));
}


private Dictionary<TKey, TValue> Create<TKey, TValue>(TKey key, TValue value) where TKey : notnull
{
return new Dictionary<TKey, TValue>
{
[key] = value
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.0.0-alpha.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.3.2" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
Expand Down
96 changes: 96 additions & 0 deletions src/Extended.Collections/DictionaryExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using Extended.Collections.Exceptions;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;

namespace Extended.Collections
{
/// <summary>
/// Contains extension methods for working with <see cref="IDictionary{TKey, TValue}"/>
/// </summary>
public static class BaseDictionaryExtension
{
[ExcludeFromCodeCoverage]
public static IDictionary<TKey,TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, IEqualityComparer<TKey>? equalityComparer, IDictionary<TKey, TValue> target)
=> Merge(source, MergeMethod.KeepLast, equalityComparer, new[] { target });

[ExcludeFromCodeCoverage]
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, IDictionary<TKey, TValue> target)
=> Merge(source, MergeMethod.KeepLast, null, new[] { target });

[ExcludeFromCodeCoverage]
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, MergeMethod method, IDictionary<TKey, TValue> target)
=> Merge(source, method, null, new IDictionary<TKey, TValue>[] { target });


[ExcludeFromCodeCoverage]
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, params IDictionary<TKey, TValue>[] targets)
=> Merge(source, MergeMethod.KeepLast, null, targets);

/// <inheritdoc cref="Merge{TKey, TValue}(IDictionary{TKey, TValue}, MergeMethod, IEqualityComparer{TKey}?, IDictionary{TKey, TValue}[])"/>
[ExcludeFromCodeCoverage]
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, MergeMethod mergeMethod, params IDictionary<TKey, TValue>[] targets)
=> Merge(source, mergeMethod, null, targets);

/// <summary>
/// Merges instances of <see cref="IDictionary{TKey, TValue}"/> into a unifed instance with the option of choosing how they are merged.
/// </summary>
/// <typeparam name="TKey">The key type</typeparam>
/// <typeparam name="TValue">The value type to store</typeparam>
/// <param name="source">The base value to start merging with.</param>
/// <param name="equalityComparer">How keys will be compaired between two dictionaries</param>
/// <param name="targets">The target instances to merge into the dictionary</param>
/// <returns>The result of the merging of objects</returns>
/// <exception cref="ArgumentNullException">The <paramref name="source"/> was null</exception>
/// <exception cref="DuplicateKeyException">Two dictionaries contained the same key and the <paramref name="mergeMethod"/> was set to <see cref="MergeMethod.Throw"/></exception>
[ExcludeFromCodeCoverage]
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, IEqualityComparer<TKey>? equalityComparer, params IDictionary<TKey, TValue>[] targets)
=> Merge(source, MergeMethod.KeepLast, equalityComparer, targets);

/// <summary>
/// Merges instances of <see cref="IDictionary{TKey, TValue}"/> into a unifed instance with the option of choosing how they are merged.
/// </summary>
/// <typeparam name="TKey">The key type</typeparam>
/// <typeparam name="TValue">The value type to store</typeparam>
/// <param name="source">The base value to start merging with.</param>
/// <param name="mergeMethod">The mergeMethod which is used to merge duplicate keys</param>
/// <param name="equalityComparer">How keys will be compaired between two dictionaries</param>
/// <param name="targets">The target instances to merge into the dictionary</param>
/// <returns>The result of the merging of objects</returns>
/// <exception cref="ArgumentNullException">The <paramref name="source"/> was null</exception>
/// <exception cref="DuplicateKeyException">Two dictionaries contained the same key and the <paramref name="mergeMethod"/> was set to <see cref="MergeMethod.Throw"/></exception>
public static IDictionary<TKey, TValue> Merge<TKey, TValue>(this IDictionary<TKey, TValue> source, MergeMethod mergeMethod, IEqualityComparer<TKey>? equalityComparer, params IDictionary<TKey, TValue>[] targets)
{
if(source == null) throw new ArgumentNullException(nameof(source));

equalityComparer ??= EqualityComparer<TKey>.Default;

IDictionary<TKey, TValue> merged = new Dictionary<TKey, TValue>(source, equalityComparer);

foreach (IDictionary<TKey, TValue> other in targets)
{
foreach(KeyValuePair<TKey, TValue> pair in other)
{
if (merged.ContainsKey(pair.Key))
{
switch(mergeMethod)
{
case MergeMethod.KeepFirst:
continue;
case MergeMethod.KeepLast:
merged[pair.Key] = pair.Value;
break;
case MergeMethod.Throw:
throw new DuplicateKeyException(pair.Key);
}
}
else
{
merged.Add(pair.Key, pair.Value);
}
}
}
return merged;
}
}
}
14 changes: 14 additions & 0 deletions src/Extended.Collections/Exceptions/DuplicateKeyException.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System;

namespace Extended.Collections.Exceptions
{
public class DuplicateKeyException : Exception
{
public object? Key { get; }

public DuplicateKeyException(object? key) : base($"The key {key} already exists in the collection")
{
Key = key;
}
}
}
4 changes: 2 additions & 2 deletions src/Extended.Collections/Extended.Collections.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.SourceLink.Common" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All"/>
<PackageReference Include="Microsoft.SourceLink.Common" Version="1.1.1" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
</ItemGroup>
</Project>
23 changes: 23 additions & 0 deletions src/Extended.Collections/MergeMethod.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Extended.Collections.Exceptions;

namespace Extended.Collections
{
/// <summary>
/// Describes the different methods used for merging key value pairs
/// </summary>
public enum MergeMethod
{
/// <summary>
/// If two values share the same key throw a <see cref="DuplicateKeyException"/> will be thrown
/// </summary>
Throw,
/// <summary>
/// Keep the first value and discard any future changes
/// </summary>
KeepFirst,
/// <summary>
/// Discard the current value and use the new one.
/// </summary>
KeepLast,
}
}

0 comments on commit 36c4187

Please sign in to comment.