From 977d6e1f3da0286178f6cac7e82758054923509d Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 07:04:16 +0100 Subject: [PATCH 1/8] Added ObservableCollection sort --- src/MADE.Collections/CollectionExtensions.cs | 27 +++++++ .../Tests/CollectionExtensionsTests.cs | 81 +++++++++++++++++++ 2 files changed, 108 insertions(+) diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index 18048c7f..8ae73578 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -5,6 +5,7 @@ namespace MADE.Collections { using System; using System.Collections.Generic; + using System.Collections.ObjectModel; using System.Linq; /// @@ -289,6 +290,8 @@ public static IEnumerable TakeFrom(this List list, int startingIndex, i { var results = new List(); + results.Sort(); + int itemsToTake = takeCount; if (list.Count - 1 - startingIndex > itemsToTake) @@ -396,5 +399,29 @@ public static IEnumerable Shuffle(this IEnumerable source) { return source.OrderBy(x => Guid.NewGuid()); } + + /// Sorts the elements in the entire using the specified comparer. + /// The source collection to sort. + /// The implementation to use when comparing elements. + /// The type of item in the collection. + /// The key value of the item to sort on. + public static void Sort(this ObservableCollection source, Func comparer) + { + if (source is not { Count: > 1 }) + { + return; + } + + var idx = 0; + foreach (var originalIdx in source.OrderBy(comparer).Select(source.IndexOf)) + { + if (originalIdx != idx) + { + source.Move(originalIdx, idx); + } + + idx++; + } + } } } \ No newline at end of file diff --git a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs index f434484f..8151d989 100644 --- a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs +++ b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs @@ -224,5 +224,86 @@ public void ShouldReturnFalseForInvalidCases(Collection expected, Collectio new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3, 4}}, }; } + + public class WhenSortingObservableCollections + { + [Test] + public void ShouldSortBySimpleType() + { + // Arrange + var collection = new ObservableCollection {3, 2, 1}; + + // Act + collection.Sort(x => x); + + // Assert + collection.ShouldBe(new[] {1, 2, 3}); + } + + [Test] + public void ShouldSortByComplexType() + { + // Arrange + var collection = new ObservableCollection + { + new() {Id = 0, Name = "James Croft"}, + new() {Id = 1, Name = "Guy Wilmer"}, + new() {Id = 2, Name = "Ben Hartley"}, + new() {Id = 3, Name = "Adam Llewellyn"}, + }; + + // Act + collection.Sort(x => x.Name); + + // Assert + collection.ShouldBe(new ComplexObject[] + { + new() {Id = 3, Name = "Adam Llewellyn"}, new() {Id = 2, Name = "Ben Hartley"}, + new() {Id = 1, Name = "Guy Wilmer"}, new() {Id = 0, Name = "James Croft"}, + }); + } + + private class ComplexObject : IEquatable + { + public int Id { get; set; } + + public string Name { get; set; } + + public bool Equals(ComplexObject other) + { + if (ReferenceEquals(null, other)) + { + return false; + } + + if (ReferenceEquals(this, other)) + { + return true; + } + + return this.Id == other.Id && this.Name == other.Name; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + { + return false; + } + + if (ReferenceEquals(this, obj)) + { + return true; + } + + return obj.GetType() == this.GetType() && this.Equals((ComplexObject)obj); + } + + public override int GetHashCode() + { + return HashCode.Combine(this.Id, this.Name); + } + } + } } } \ No newline at end of file From 3b525d7f95c0ad331819c7d2f6f5b4c3622b6594 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 07:05:47 +0100 Subject: [PATCH 2/8] Removed irrelevant sort on new collection --- src/MADE.Collections/CollectionExtensions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index 8ae73578..e4d2da7d 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -290,8 +290,6 @@ public static IEnumerable TakeFrom(this List list, int startingIndex, i { var results = new List(); - results.Sort(); - int itemsToTake = takeCount; if (list.Count - 1 - startingIndex > itemsToTake) From 0eaecec4d0bc3fdcb69b573a2ea910c7fc1f8c40 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 07:09:18 +0100 Subject: [PATCH 3/8] Added ObservableCollection sort descending --- src/MADE.Collections/CollectionExtensions.cs | 24 +++++++++ .../Tests/CollectionExtensionsTests.cs | 54 ++++++++++++++++--- 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index e4d2da7d..c298c85c 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -421,5 +421,29 @@ public static void Sort(this ObservableCollection source, FuncSorts the elements in the entire using the specified comparer in descending order. + /// The source collection to sort. + /// The implementation to use when comparing elements. + /// The type of item in the collection. + /// The key value of the item to sort on. + public static void SortDescending(this ObservableCollection source, Func comparer) + { + if (source is not { Count: > 1 }) + { + return; + } + + var idx = 0; + foreach (var originalIdx in source.OrderByDescending(comparer).Select(source.IndexOf)) + { + if (originalIdx != idx) + { + source.Move(originalIdx, idx); + } + + idx++; + } + } } } \ No newline at end of file diff --git a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs index 8151d989..1dd791f4 100644 --- a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs +++ b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs @@ -75,7 +75,7 @@ public void ShouldThrowArgumentNullExceptionIfNullCollection() public void ShouldThrowArgumentNullExceptionIfNullItem() { // Arrange - var list = new List {"Hello"}; + var list = new List { "Hello" }; string item = null; // Act & Assert @@ -89,7 +89,7 @@ public void ShouldReturnTrueIfItemUpdated() TestObject objectToAdd = TestObjectFaker.Create().Generate(); TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); - var list = new List {objectToAdd}; + var list = new List { objectToAdd }; // Act bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToAdd.Name); @@ -105,7 +105,7 @@ public void ShouldReturnFalseIfItemToUpdateDoesNotExist() TestObject objectToAdd = TestObjectFaker.Create().Generate(); TestObject objectToUpdateWith = TestObjectFaker.Create().Generate(); - var list = new List {objectToAdd}; + var list = new List { objectToAdd }; // Act bool updated = list.Update(objectToUpdateWith, (s, i) => s.Name == objectToUpdateWith.Name); @@ -131,7 +131,7 @@ public void ShouldThrowArgumentNullExceptionIfNullCollection() public void ShouldThrowArgumentNullExceptionIfNullSource() { // Arrange - var list = new List {"Hello"}; + var list = new List { "Hello" }; // Act & Assert Assert.Throws(() => list.MakeEqualTo(null)); @@ -141,8 +141,8 @@ public void ShouldThrowArgumentNullExceptionIfNullSource() public void ShouldUpdateCollectionToBeEqualOther() { // Arrange - var list = new List {"Hello"}; - var update = new List {"New", "List"}; + var list = new List { "Hello" }; + var update = new List { "New", "List" }; // Act list.MakeEqualTo(update); @@ -231,13 +231,13 @@ public class WhenSortingObservableCollections public void ShouldSortBySimpleType() { // Arrange - var collection = new ObservableCollection {3, 2, 1}; + var collection = new ObservableCollection { 3, 2, 1 }; // Act collection.Sort(x => x); // Assert - collection.ShouldBe(new[] {1, 2, 3}); + collection.ShouldBe(new[] { 1, 2, 3 }); } [Test] @@ -263,6 +263,44 @@ public void ShouldSortByComplexType() }); } + [Test] + public void ShouldSortDescendingBySimpleType() + { + // Arrange + var collection = new ObservableCollection { 2, 1, 3 }; + + // Act + collection.SortDescending(x => x); + + // Assert + collection.ShouldBe(new[] { 3, 2, 1 }); + } + + [Test] + public void ShouldSortDescendingByComplexType() + { + // Arrange + var collection = new ObservableCollection + { + new() {Id = 0, Name = "Ben Hartley"}, + new() {Id = 1, Name = "James Croft"}, + new() {Id = 2, Name = "Adam Llewellyn"}, + new() {Id = 3, Name = "Guy Wilmer"}, + }; + + // Act + collection.SortDescending(x => x.Name); + + // Assert + collection.ShouldBe(new ComplexObject[] + { + new() {Id = 1, Name = "James Croft"}, + new() {Id = 3, Name = "Guy Wilmer"}, + new() {Id = 0, Name = "Ben Hartley"}, + new() {Id = 2, Name = "Adam Llewellyn"}, + }); + } + private class ComplexObject : IEquatable { public int Id { get; set; } From b9fe2643407681ae2dda2424d58b46201d8b2c15 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 07:20:40 +0100 Subject: [PATCH 4/8] Added collection to delimited string converter extension --- .../Extensions/CollectionExtensions.cs | 25 +++++++++++ .../Tests/CollectionExtensionsTests.cs | 41 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 src/MADE.Data.Converters/Extensions/CollectionExtensions.cs create mode 100644 tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs diff --git a/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs b/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs new file mode 100644 index 00000000..25263859 --- /dev/null +++ b/src/MADE.Data.Converters/Extensions/CollectionExtensions.cs @@ -0,0 +1,25 @@ +// MADE Apps licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace MADE.Data.Converters.Extensions +{ + using System.Collections.Generic; + + /// + /// Defines a collection of extensions for collection objects. + /// + public static class CollectionExtensions + { + /// + /// Converts a collection of items to a string separated by a delimiter. + /// + /// The type of item within the collection. + /// The source collection to convert. + /// The delimiter to separate items by in the string. Default, comma. + /// A delimited string representing the collection. + public static string ToDelimitedString(this IEnumerable source, string delimiter = ",") + { + return string.Join(delimiter, source); + } + } +} \ No newline at end of file diff --git a/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs new file mode 100644 index 00000000..446c522d --- /dev/null +++ b/tests/MADE.Data.Converters.Tests/Tests/CollectionExtensionsTests.cs @@ -0,0 +1,41 @@ +namespace MADE.Data.Converters.Tests.Tests +{ + using System.Diagnostics.CodeAnalysis; + using MADE.Data.Converters.Extensions; + using NUnit.Framework; + using Shouldly; + + [ExcludeFromCodeCoverage] + [TestFixture] + public class CollectionExtensionsTests + { + public class WhenConvertingToDelimitedString + { + [Test] + public void ShouldReturnItemsWithCommaDelimiterByDefault() + { + // Arrange + var items = new[] { "item1", "item2", "item3" }; + + // Act + var result = items.ToDelimitedString(); + + // Assert + result.ShouldBe("item1,item2,item3"); + } + + [Test] + public void ShouldReturnItemsWithCustomDelimiter() + { + // Arrange + var items = new[] { "item1", "item2", "item3" }; + + // Act + var result = items.ToDelimitedString(delimiter: " | "); + + // Assert + result.ShouldBe("item1 | item2 | item3"); + } + } + } +} \ No newline at end of file From 0a92932bfbd41c46963ad8a1ff6a49488f0dba75 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 07:31:41 +0100 Subject: [PATCH 5/8] Added collection IsNullOrEmpty extension --- src/MADE.Collections/CollectionExtensions.cs | 11 +++ .../Tests/CollectionExtensionsTests.cs | 69 +++++++++++++++++-- 2 files changed, 76 insertions(+), 4 deletions(-) diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index c298c85c..07b5f453 100644 --- a/src/MADE.Collections/CollectionExtensions.cs +++ b/src/MADE.Collections/CollectionExtensions.cs @@ -445,5 +445,16 @@ public static void SortDescending(this ObservableCollection source, idx++; } } + + /// Indicates whether the specified collection is or empty (containing no items). + /// The collection to test. + /// The type of item in the collection. + /// + /// if the parameter is or empty (containing no items); otherwise, . + /// + public static bool IsNullOrEmpty(this IEnumerable source) + { + return source is null || !source.Any(); + } } } \ No newline at end of file diff --git a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs index 1dd791f4..2bfb514b 100644 --- a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs +++ b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs @@ -225,6 +225,69 @@ public void ShouldReturnFalseForInvalidCases(Collection expected, Collectio }; } + public class WhenValidatingIfCollectionIsNullOrEmpty + { + [TestCaseSource(nameof(ValidEnumerableCases))] + public void ShouldReturnTrueIfEnumerableIsNullOrEmpty(IEnumerable collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); + + // Assert + isEmpty.ShouldBeTrue(); + } + + [TestCaseSource(nameof(ValidDictionaryCases))] + public void ShouldReturnTrueIfDictionaryIsNullOrEmpty(Dictionary collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); + + // Assert + isEmpty.ShouldBeTrue(); + } + + [TestCaseSource(nameof(InvalidEnumerableCases))] + public void ShouldReturnFalseIfEnumerableIsNotNullOrEmpty(IEnumerable collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); + + // Assert + isEmpty.ShouldBeFalse(); + } + + [TestCaseSource(nameof(InvalidDictionaryCases))] + public void ShouldReturnFalseIfDictionaryIsNotNullOrEmpty(Dictionary collection) + { + // Act + bool isEmpty = collection.IsNullOrEmpty(); + + // Assert + isEmpty.ShouldBeFalse(); + } + + private static object[] ValidEnumerableCases = + { + new object[] {null}, new object[] {new ObservableCollection()}, + }; + + private static object[] ValidDictionaryCases = + { + new object[] {null}, new object[] {new Dictionary()}, + }; + + private static object[] InvalidEnumerableCases = + { + new object[] {new ObservableCollection {1, 2, 3}}, + }; + + private static object[] InvalidDictionaryCases = + { + new object[] {new Dictionary {{1, "A"}, {2, "B"}, {3, "C"}}}, + }; + } + public class WhenSortingObservableCollections { [Test] @@ -294,10 +357,8 @@ public void ShouldSortDescendingByComplexType() // Assert collection.ShouldBe(new ComplexObject[] { - new() {Id = 1, Name = "James Croft"}, - new() {Id = 3, Name = "Guy Wilmer"}, - new() {Id = 0, Name = "Ben Hartley"}, - new() {Id = 2, Name = "Adam Llewellyn"}, + new() {Id = 1, Name = "James Croft"}, new() {Id = 3, Name = "Guy Wilmer"}, + new() {Id = 0, Name = "Ben Hartley"}, new() {Id = 2, Name = "Adam Llewellyn"}, }); } From 10082d5bd1a297514c0d570c241ee2cf5feac468 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 08:25:53 +0100 Subject: [PATCH 6/8] Added length conversion extensions for meters-miles --- .../Extensions/LengthExtensions.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 src/MADE.Data.Converters/Extensions/LengthExtensions.cs diff --git a/src/MADE.Data.Converters/Extensions/LengthExtensions.cs b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs new file mode 100644 index 00000000..282941ea --- /dev/null +++ b/src/MADE.Data.Converters/Extensions/LengthExtensions.cs @@ -0,0 +1,28 @@ +namespace MADE.Data.Converters.Extensions +{ + /// + /// Defines a collection of extensions for converting length measurements. + /// + public static class LengthExtensions + { + /// + /// Converts a distance measured in miles to a distance measured in meters. + /// + /// The miles to convert to meters. + /// The meters that represent the miles. + public static double ToMeters(this double miles) + { + return miles * 1609.344; + } + + /// + /// Converts a distance measured in meters to a distance measured in miles. + /// + /// The meters to convert to miles. + /// The miles that represent the meters. + public static double ToMiles(this double meters) + { + return meters / 1609.344; + } + } +} \ No newline at end of file From 93cee792d2600dd7e095206322298e1a8172914a Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 08:28:58 +0100 Subject: [PATCH 7/8] Added GetValueOrDefault extension for dictionaries --- src/MADE.Collections/DictionaryExtensions.cs | 24 ++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/MADE.Collections/DictionaryExtensions.cs b/src/MADE.Collections/DictionaryExtensions.cs index 7b4471d9..3d4fab85 100644 --- a/src/MADE.Collections/DictionaryExtensions.cs +++ b/src/MADE.Collections/DictionaryExtensions.cs @@ -49,5 +49,29 @@ public static void AddOrUpdate(this Dictionary dicti dictionary.Add(key, value); } + + /// + /// Gets a value from a dictionary by the specified key, or returns a default value. + /// + /// The type of key item within the dictionary. + /// The type of value item within the dictionary. + /// The dictionary to get a value from. + /// The key to get a value for. + /// The default value to return if not exists. Default, null. + /// The value if it exists for the key; otherwise, null. + public static TValue GetValueOrDefault( + this Dictionary dictionary, + TKey key, + TValue defaultValue = default) + { + var result = defaultValue; + + if (dictionary != null && dictionary.ContainsKey(key)) + { + result = dictionary[key]; + } + + return result; + } } } \ No newline at end of file From f8d2ee68ce51d1993716bf70be903e57399548b9 Mon Sep 17 00:00:00 2001 From: James Croft Date: Tue, 24 May 2022 08:32:29 +0100 Subject: [PATCH 8/8] Added extension to get property names from an object --- .../Extensions/ReflectionExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs index 7c65ba8e..e320f1fe 100644 --- a/src/MADE.Runtime/Extensions/ReflectionExtensions.cs +++ b/src/MADE.Runtime/Extensions/ReflectionExtensions.cs @@ -1,6 +1,7 @@ namespace MADE.Runtime.Extensions { using System; + using System.Collections.Generic; using System.Reflection; /// @@ -23,5 +24,19 @@ public static T GetPropertyValue(this object obj, string property) PropertyInfo prop = type.GetProperty(property); return prop?.GetValue(obj) as T; } + + /// + /// Gets all the property names declared for the specified object. + /// + /// The object to retrieve property names from. + /// A collection of object property names as a string. + public static IEnumerable GetPropertyNames(this object obj) + { + Type type = obj.GetType(); + foreach (PropertyInfo property in type.GetTypeInfo().DeclaredProperties) + { + yield return property.Name; + } + } } } \ No newline at end of file