diff --git a/src/MADE.Collections/CollectionExtensions.cs b/src/MADE.Collections/CollectionExtensions.cs index 18048c7f..07b5f453 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; /// @@ -396,5 +397,64 @@ 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++; + } + } + + /// Sorts 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++; + } + } + + /// 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/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 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/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 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 diff --git a/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs b/tests/MADE.Collections.Tests/Tests/CollectionExtensionsTests.cs index f434484f..2bfb514b 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); @@ -224,5 +224,185 @@ public void ShouldReturnFalseForInvalidCases(Collection expected, Collectio new object[] {new ObservableCollection {1, 2, 3}, new ObservableCollection {1, 2, 3, 4}}, }; } + + 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] + 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"}, + }); + } + + [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; } + + 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 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