diff --git a/DNX.Extensions.sln.DotSettings b/DNX.Extensions.sln.DotSettings index af0543c..dcd6643 100644 --- a/DNX.Extensions.sln.DotSettings +++ b/DNX.Extensions.sln.DotSettings @@ -1,4 +1,5 @@ <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> + <s:Boolean x:Key="/Default/UserDictionary/Words/=oversized/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Populatable/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=Southend/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/UserDictionary/Words/=WORDIFY/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> \ No newline at end of file diff --git a/src/DNX.Extensions/Arrays/ArrayExtensions.cs b/src/DNX.Extensions/Arrays/ArrayExtensions.cs index 87bdf06..e1d46a1 100644 --- a/src/DNX.Extensions/Arrays/ArrayExtensions.cs +++ b/src/DNX.Extensions/Arrays/ArrayExtensions.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; #pragma warning disable 1591 @@ -65,4 +67,22 @@ public static T[] ShiftRight<T>(this T[] input, int by = 1, T fillValue = defaul return shiftedArray; } + + public static T[] Reduce<T>(this T[] inputArray, int targetSize, Func<T, T, T> method) + { + if (inputArray.Length <= targetSize) + return inputArray; + + var result = new List<T>(inputArray.Take(targetSize)); + + for (var sourceIndex = targetSize; sourceIndex < inputArray.Length; ++sourceIndex) + { + var targetIndex = sourceIndex % targetSize; + + var value = method(result[targetIndex], inputArray[sourceIndex]); + result[targetIndex] = value; + } + + return result.ToArray(); + } } diff --git a/src/DNX.Extensions/Conversion/GuidExtensions.cs b/src/DNX.Extensions/Conversion/GuidExtensions.cs index 283406d..2b23082 100644 --- a/src/DNX.Extensions/Conversion/GuidExtensions.cs +++ b/src/DNX.Extensions/Conversion/GuidExtensions.cs @@ -1,6 +1,9 @@ using System; using System.Security.Cryptography; using System.Text; +using DNX.Extensions.Arrays; + +// ReSharper disable InconsistentNaming namespace DNX.Extensions.Conversion; @@ -9,26 +12,53 @@ namespace DNX.Extensions.Conversion; /// </summary> public static class GuidExtensions { - /// <summary> - /// Convert any text item to a guid. - /// </summary> + public const int MAX_GUID_BYTE_ARRAY_SIZE = 16; + + private static byte KeepOriginalValue(byte value1, byte value2) => value1; + private static byte ExclusiveOr(byte value1, byte value2) => (byte)(value1 ^ value2); + + /// <summary>Convert any text item to a guid.</summary> + /// <param name="input">The text to convert</param> + /// <returns>A <see cref="Guid" /></returns> /// <remarks> /// The result is deterministic in that each text item will always generate the same result /// NOTE: If the text item is actually a Guid, it will NOT be parsed directly to a Guid /// </remarks> - /// <param name="input"></param> - /// <returns> - /// A <see cref="Guid"/> - /// </returns> public static Guid ToDeterministicGuid(this string input) { - input ??= string.Empty; + return ToDeterministicGuid(input, MD5.Create(), KeepOriginalValue); + } - //use MD5 hash to get a 16-byte hash of the string: - using var provider = new MD5CryptoServiceProvider(); + /// <summary>Convert any text item to a guid.</summary> + /// <param name="input">The text to convert</param> + /// <param name="provider">The hash provider.</param> + /// <returns>A <see cref="Guid" /></returns> + /// <remarks> + /// The result is deterministic in that each text item will always generate the same result + /// NOTE: If the text item is actually a Guid, it will NOT be parsed directly to a Guid + /// </remarks> + public static Guid ToDeterministicGuid(this string input, HashAlgorithm provider) + { + return ToDeterministicGuid(input, provider, KeepOriginalValue); + } + + /// <summary>Convert any text item to a guid.</summary> + /// <param name="input">The text to convert</param> + /// <param name="provider">The hash provider.</param> + /// <param name="reduceMethod">The method to use to reduce an oversized byte array to something a GUID will accept.</param> + /// <returns>A <see cref="Guid" /></returns> + /// <remarks> + /// The result is deterministic in that each text item will always generate the same result + /// NOTE: If the text item is actually a Guid, it will NOT be parsed directly to a Guid + /// </remarks> + public static Guid ToDeterministicGuid(this string input, HashAlgorithm provider, Func<byte, byte, byte> reduceMethod) + { + input ??= string.Empty; var inputBytes = Encoding.Default.GetBytes(input); + var hashBytes = provider.ComputeHash(inputBytes); + hashBytes = hashBytes.Reduce(MAX_GUID_BYTE_ARRAY_SIZE, reduceMethod); //generate a guid from the hash: var hashGuid = new Guid(hashBytes); diff --git a/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs b/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs index eb944ba..0b22580 100644 --- a/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs +++ b/tests/DNX.Extensions.Tests/Arrays/ArrayExtensionsTests.cs @@ -3,6 +3,8 @@ using Xunit; using Xunit.Abstractions; +// ReSharper disable UseUtf8StringLiteral + #pragma warning disable IDE0290 namespace DNX.Extensions.Tests.Arrays; @@ -114,7 +116,6 @@ public void Test_ShiftLeft_null_array() } } - public class ShiftRight { [Fact] @@ -170,4 +171,92 @@ public void Test_ShiftRight_null_array() result.Length.Should().Be(0); } } + + public class Reduce + { + [Theory] + [MemberData(nameof(Reduce_Data_byte))] + public void Reduce_can_produce_an_appropriate_output_for_bytes(byte[] input, int targetSize, Func<byte, byte, byte> method, byte[] expectedResult) + { + // Act + var result = input.Reduce(targetSize, method); + + // Assert + result.Length.Should().BeLessThanOrEqualTo(targetSize); + result.Should().BeEquivalentTo(expectedResult); + } + + [Theory] + [MemberData(nameof(Reduce_Data_string))] + public void Reduce_can_produce_an_appropriate_output_for_strings(string[] input, int targetSize, Func<string, string, string> method, string[] expectedResult) + { + // Act + var result = input.Reduce(targetSize, method); + + // Assert + result.Length.Should().BeLessThanOrEqualTo(targetSize); + result.Should().BeEquivalentTo(expectedResult); + } + + // Useful : https://www.compscilib.com/calculate/binaryxor?variation=default + public static TheoryData<byte[], int, Func<byte, byte, byte>, byte[]> Reduce_Data_byte() + { + var data = new TheoryData<byte[], int, Func<byte, byte, byte>, byte[]>(); + + data.Add( + [1, 2, 3, 4, 5, 6], + 6, + Or, + [1, 2, 3, 4, 5, 6] + ); + data.Add( + [1, 2, 3, 4, 5, 6], + 8, + Or, + [1, 2, 3, 4, 5, 6] + ); + data.Add( + [1, 2, 3, 4, 5, 6], + 3, + Or, + [5, 7, 7] + ); + data.Add( + [1, 2, 3, 4, 5, 6], + 3, + XOr, + [5, 7, 5] + ); + data.Add( + [10, 20, 30, 40, 50, 60], + 3, + XOr, + [34, 38, 34] + ); + + return data; + } + + public static TheoryData<string[], int, Func<string, string, string>, string[]> Reduce_Data_string() + { + var data = new TheoryData<string[], int, Func<string, string, string>, string[]>(); + + data.Add( + ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"], + 5, + Concat, + ["AF", "BG", "CH", "DI", "EJ"] + ); + + return data; + } + + public static byte Or(byte value1, byte value2) => (byte)(value1 | value2); + public static byte XOr(byte value1, byte value2) => (byte)(value1 ^ value2); + public static byte And(byte value1, byte value2) => (byte)(value1 & value2); + public static byte Complement1(byte value1, byte value2) => (byte)(~value1); + public static byte Complement2(byte value1, byte value2) => (byte)(~value2); + + public static string Concat(string value1, string value2) => value1 + value2; + } } diff --git a/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs b/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs index 53a37d7..b17896b 100644 --- a/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs +++ b/tests/DNX.Extensions.Tests/Conversion/GuidExtensionsTests.cs @@ -1,3 +1,4 @@ +using System.Security.Cryptography; using DNX.Extensions.Conversion; using FluentAssertions; using Xunit; @@ -24,6 +25,19 @@ public void ToDeterministicGuid_can_create_a_guid_from_any_string(string text) result.ToString().Should().NotBe(text); } + [Theory] + [MemberData(nameof(ToDeterministicGuid_custom_algorithm_Data))] + public void ToDeterministicGuid_can_create_a_guid_from_any_string_with_a_custom_algorithm(string text, HashAlgorithm algorithm) + { + // Act + var result = text.ToDeterministicGuid(algorithm); + outputHelper.WriteLine($"Text: {text} = {result}"); + + // Assert + result.Should().NotBe(Guid.Empty); + result.ToString().Should().NotBe(text); + } + [Theory] [InlineData("ABC", "d2bd2f90-dfb1-4f0c-70b4-a5d23525e932")] [InlineData("abc", "98500190-d23c-b04f-d696-3f7d28e17f72")] @@ -44,4 +58,28 @@ public void ToDeterministicGuid_will_always_generate_a_predictable_result(string result.Should().Be(result2); result.ToString().Should().Be(expected); } -} \ No newline at end of file + + // Useful : https://www.compscilib.com/calculate/binaryxor?variation=default + public static TheoryData<string, HashAlgorithm> ToDeterministicGuid_custom_algorithm_Data() + { + var algorithms = new HashAlgorithm[] + { + MD5.Create(), + SHA1.Create(), + SHA256.Create(), + }; + + var data = new TheoryData<string, HashAlgorithm>(); + + foreach (var algorithm in algorithms) + { + data.Add("ABC", algorithm); + data.Add("abc", algorithm); + data.Add("", algorithm); + data.Add(null, algorithm); + data.Add("497111F7-1511-49A8-8DB2-31B5E40953CB", algorithm); + } + + return data; + } +}