diff --git a/PowerCSharp.sln b/PowerCSharp.sln index 561d853..c7a1d94 100644 --- a/PowerCSharp.sln +++ b/PowerCSharp.sln @@ -47,6 +47,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerCSharp.Feature.Cache.A EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerCSharp.Feature.Cache.Disk", "src\Features\PowerCSharp.Feature.Cache.Disk\PowerCSharp.Feature.Cache.Disk.csproj", "{137976EF-A677-499E-9567-A8B7E3B6F1BE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerCSharp.Core.Tests", "tests\PowerCSharp.Core.Tests\PowerCSharp.Core.Tests.csproj", "{F5B563E8-E215-4350-9480-BBFCE47DCF34}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerCSharp.Utilities.Tests", "tests\PowerCSharp.Utilities.Tests\PowerCSharp.Utilities.Tests.csproj", "{79C80B63-F711-4EC4-91F5-52D892E60DC6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -128,6 +132,14 @@ Global {137976EF-A677-499E-9567-A8B7E3B6F1BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {137976EF-A677-499E-9567-A8B7E3B6F1BE}.Release|Any CPU.ActiveCfg = Release|Any CPU {137976EF-A677-499E-9567-A8B7E3B6F1BE}.Release|Any CPU.Build.0 = Release|Any CPU + {F5B563E8-E215-4350-9480-BBFCE47DCF34}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F5B563E8-E215-4350-9480-BBFCE47DCF34}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F5B563E8-E215-4350-9480-BBFCE47DCF34}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F5B563E8-E215-4350-9480-BBFCE47DCF34}.Release|Any CPU.Build.0 = Release|Any CPU + {79C80B63-F711-4EC4-91F5-52D892E60DC6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {79C80B63-F711-4EC4-91F5-52D892E60DC6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {79C80B63-F711-4EC4-91F5-52D892E60DC6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {79C80B63-F711-4EC4-91F5-52D892E60DC6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {73835311-BC0A-4107-9E6D-4950DF565C46} = {493BDE4C-2EBA-49EE-BF44-10AAB73634D6} @@ -149,5 +161,7 @@ Global {01C67F51-0FE0-4576-9507-7CE856370C14} = {8507D629-2649-468E-8345-212CFC547FA8} {E61A4C83-42A8-42A5-B1F1-868ED5DE762F} = {006F80FE-AC82-4752-972B-081BA6C6A651} {137976EF-A677-499E-9567-A8B7E3B6F1BE} = {006F80FE-AC82-4752-972B-081BA6C6A651} + {F5B563E8-E215-4350-9480-BBFCE47DCF34} = {8507D629-2649-468E-8345-212CFC547FA8} + {79C80B63-F711-4EC4-91F5-52D892E60DC6} = {8507D629-2649-468E-8345-212CFC547FA8} EndGlobalSection EndGlobal diff --git a/src/PowerCSharp.Core/Collections/ListExtensions.cs b/src/PowerCSharp.Core/Collections/ListExtensions.cs index 39c6888..88058eb 100644 --- a/src/PowerCSharp.Core/Collections/ListExtensions.cs +++ b/src/PowerCSharp.Core/Collections/ListExtensions.cs @@ -38,12 +38,39 @@ public static class ListExtensions var clonedList = new List(sourceList.Count); foreach (var item in sourceList) { - if (item != null) + clonedList.Add(item != null ? (T)item.Clone() : default); + } + + return clonedList; + } + + /// + /// Returns a new list containing only the first occurrence of each unique item based on the specified key selector. + /// + /// The type of elements in the list. + /// The type of the key to compare for uniqueness. + /// The source list to deduplicate. + /// A function to extract the key for each element. + /// A new list with duplicates removed based on the specified key, or null if the input is null. + public static List? DistinctBy(this List? sourceList, Func keySelector) + { + if (sourceList == null) + { + return null; + } + + var seenKeys = new HashSet(); + var result = new List(); + + foreach (var item in sourceList) + { + var key = keySelector(item); + if (seenKeys.Add(key)) { - clonedList.Add((T)item.Clone()); + result.Add(item); } } - return clonedList; + return result; } } diff --git a/src/PowerCSharp.Extensions/Strings/StringExtensions.cs b/src/PowerCSharp.Extensions/Strings/StringExtensions.cs index 88aee76..a7d31a4 100644 --- a/src/PowerCSharp.Extensions/Strings/StringExtensions.cs +++ b/src/PowerCSharp.Extensions/Strings/StringExtensions.cs @@ -661,6 +661,17 @@ public static string Convert9DigitZipTo5Digit(this string zipCode) ?? zipCode; } + /// + /// Returns the raw value when it has content; otherwise the current fallback value. + /// + /// The candidate raw value. + /// The value to keep when the candidate is empty. + /// The overriding value or the existing fallback. + public static string? Coalesce(this string? rawValue, string? fallback) + { + return string.IsNullOrEmpty(rawValue) ? fallback : rawValue; + } + /// /// Returns a copy of the string after removing Html Tags and Entities like ® or  . /// diff --git a/tests/PowerCSharp.BuiltInFeatures.Tests/PowerCSharp.BuiltInFeatures.Tests.csproj b/tests/PowerCSharp.BuiltInFeatures.Tests/PowerCSharp.BuiltInFeatures.Tests.csproj index 62e1870..94d18e5 100644 --- a/tests/PowerCSharp.BuiltInFeatures.Tests/PowerCSharp.BuiltInFeatures.Tests.csproj +++ b/tests/PowerCSharp.BuiltInFeatures.Tests/PowerCSharp.BuiltInFeatures.Tests.csproj @@ -20,9 +20,9 @@ - - - + + + diff --git a/tests/PowerCSharp.Core.Tests/DictionaryExtensionsTests.cs b/tests/PowerCSharp.Core.Tests/DictionaryExtensionsTests.cs index 9a368bd..bbaf674 100644 --- a/tests/PowerCSharp.Core.Tests/DictionaryExtensionsTests.cs +++ b/tests/PowerCSharp.Core.Tests/DictionaryExtensionsTests.cs @@ -120,7 +120,7 @@ public void TryAdd_WithNullDictionary_ShouldThrowArgumentNullException() Dictionary? nullDictionary = null; // Act & Assert - Assert.Throws(() => nullDictionary!.TryAdd("key", 1)); + Assert.Throws(() => nullDictionary!.TryAdd("key", 1)); } [Fact] diff --git a/tests/PowerCSharp.Core.Tests/ListExtensionsTests.cs b/tests/PowerCSharp.Core.Tests/ListExtensionsTests.cs new file mode 100644 index 0000000..2de7850 --- /dev/null +++ b/tests/PowerCSharp.Core.Tests/ListExtensionsTests.cs @@ -0,0 +1,267 @@ +using PowerCSharp.Core.Collections; +using Xunit; + +namespace PowerCSharp.Core.Tests; + +public class ListExtensionsTests +{ + #region DistinctBy Tests + + [Fact] + public void DistinctBy_WithNullList_ShouldReturnNull() + { + // Arrange + List? nullList = null; + + // Act + var result = nullList.DistinctBy(x => x); + + // Assert + Assert.Null(result); + } + + [Fact] + public void DistinctBy_WithEmptyList_ShouldReturnEmptyList() + { + // Arrange + var emptyList = new List(); + + // Act + var result = emptyList.DistinctBy(x => x); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + } + + [Fact] + public void DistinctBy_WithNoDuplicates_ShouldReturnAllItems() + { + // Arrange + var list = new List { 1, 2, 3, 4, 5 }; + + // Act + var result = list.DistinctBy(x => x); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(list, result); + } + + [Fact] + public void DistinctBy_WithDuplicates_ShouldRemoveDuplicates() + { + // Arrange + var list = new List { 1, 2, 2, 3, 4, 4, 5 }; + + // Act + var result = list.DistinctBy(x => x); + + // Assert + Assert.Equal(5, result.Count); + Assert.Equal(1, result[0]); + Assert.Equal(2, result[1]); + Assert.Equal(3, result[2]); + Assert.Equal(4, result[3]); + Assert.Equal(5, result[4]); + } + + [Fact] + public void DistinctBy_WithComplexType_ShouldDedulicateBySpecifiedField() + { + // Arrange + var list = new List + { + new TestPerson { Id = 1, Name = "John" }, + new TestPerson { Id = 2, Name = "Jane" }, + new TestPerson { Id = 1, Name = "John Duplicate" }, + new TestPerson { Id = 3, Name = "Bob" }, + new TestPerson { Id = 2, Name = "Jane Duplicate" } + }; + + // Act + var result = list.DistinctBy(x => x.Id); + + // Assert + Assert.Equal(3, result.Count); + Assert.Equal(1, result[0].Id); + Assert.Equal("John", result[0].Name); // First occurrence kept + Assert.Equal(2, result[1].Id); + Assert.Equal("Jane", result[1].Name); // First occurrence kept + Assert.Equal(3, result[2].Id); + Assert.Equal("Bob", result[2].Name); + } + + [Fact] + public void DistinctBy_WithStringKey_ShouldDedulicateByStringField() + { + // Arrange + var list = new List + { + new TestPerson { Id = 1, Name = "John" }, + new TestPerson { Id = 2, Name = "Jane" }, + new TestPerson { Id = 3, Name = "John" }, + new TestPerson { Id = 4, Name = "Bob" } + }; + + // Act + var result = list.DistinctBy(x => x.Name); + + // Assert + Assert.Equal(3, result.Count); + Assert.Equal("John", result[0].Name); + Assert.Equal("Jane", result[1].Name); + Assert.Equal("Bob", result[2].Name); + } + + [Fact] + public void DistinctBy_WithAllDuplicates_ShouldReturnSingleItem() + { + // Arrange + var list = new List { 5, 5, 5, 5, 5 }; + + // Act + var result = list.DistinctBy(x => x); + + // Assert + Assert.Single(result); + Assert.Equal(5, result[0]); + } + + [Fact] + public void DistinctBy_ShouldPreserveOrderOfFirstOccurrences() + { + // Arrange + var list = new List { 3, 1, 2, 1, 3, 2 }; + + // Act + var result = list.DistinctBy(x => x); + + // Assert + Assert.Equal(3, result.Count); + Assert.Equal(3, result[0]); // First occurrence of 3 + Assert.Equal(1, result[1]); // First occurrence of 1 + Assert.Equal(2, result[2]); // First occurrence of 2 + } + + #endregion + + #region DeepClone Tests + + [Fact] + public void DeepClone_WithNullList_ShouldReturnNull() + { + // Arrange + List? nullList = null; + + // Act + var result = nullList.DeepClone(); + + // Assert + Assert.Null(result); + } + + [Fact] + public void DeepClone_WithEmptyList_ShouldReturnEmptyList() + { + // Arrange + var emptyList = new List(); + + // Act + var result = emptyList.DeepClone(); + + // Assert + Assert.NotNull(result); + Assert.Empty(result); + Assert.NotSame(emptyList, result); + } + + [Fact] + public void DeepClone_WithCloneableItems_ShouldCreateDeepCopy() + { + // Arrange + var list = new List + { + new TestCloneable { Value = 1 }, + new TestCloneable { Value = 2 }, + new TestCloneable { Value = 3 } + }; + + // Act + var result = list.DeepClone(); + + // Assert + Assert.NotNull(result); + Assert.Equal(list.Count, result.Count); + Assert.NotSame(list, result); + + for (int i = 0; i < list.Count; i++) + { + Assert.NotSame(list[i], result[i]); + Assert.Equal(list[i].Value, result[i].Value); + } + } + + [Fact] + public void DeepClone_WithNullItems_ShouldHandleNullItems() + { + // Arrange + var list = new List + { + new TestCloneable { Value = 1 }, + null, + new TestCloneable { Value = 3 } + }; + + // Act + var result = list.DeepClone(); + + // Assert + Assert.NotNull(result); + Assert.Equal(list.Count, result.Count); + Assert.NotNull(result[0]); + Assert.Null(result[1]); + Assert.NotNull(result[2]); + } + + [Fact] + public void DeepClone_ModifyingCloneShouldNotAffectOriginal() + { + // Arrange + var list = new List + { + new TestCloneable { Value = 10 }, + new TestCloneable { Value = 20 } + }; + + // Act + var result = list.DeepClone(); + result[0].Value = 999; + + // Assert + Assert.Equal(10, list[0].Value); + Assert.Equal(999, result[0].Value); + } + + #endregion + + #region Test Helper Classes + + private class TestPerson + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + } + + private class TestCloneable : ICloneable + { + public int Value { get; set; } + + public object Clone() + { + return new TestCloneable { Value = this.Value }; + } + } + + #endregion +} diff --git a/tests/PowerCSharp.Core.Tests/PowerCSharp.Core.Tests.csproj b/tests/PowerCSharp.Core.Tests/PowerCSharp.Core.Tests.csproj index 5c6f25b..9463f5d 100644 --- a/tests/PowerCSharp.Core.Tests/PowerCSharp.Core.Tests.csproj +++ b/tests/PowerCSharp.Core.Tests/PowerCSharp.Core.Tests.csproj @@ -20,8 +20,9 @@ - - + + + diff --git a/tests/PowerCSharp.Core.Tests/UnitTest1.cs b/tests/PowerCSharp.Core.Tests/StringExtensionsTests.cs similarity index 94% rename from tests/PowerCSharp.Core.Tests/UnitTest1.cs rename to tests/PowerCSharp.Core.Tests/StringExtensionsTests.cs index 7108a56..5a3775f 100644 --- a/tests/PowerCSharp.Core.Tests/UnitTest1.cs +++ b/tests/PowerCSharp.Core.Tests/StringExtensionsTests.cs @@ -1,5 +1,6 @@ using System; using PowerCSharp.Core; +using PowerCSharp.Extensions.Strings; using Xunit; namespace PowerCSharp.Core.Tests; diff --git a/tests/PowerCSharp.Extensions.Tests/StringExtensionsTests.cs b/tests/PowerCSharp.Extensions.Tests/StringExtensionsTests.cs new file mode 100644 index 0000000..86eb6ab --- /dev/null +++ b/tests/PowerCSharp.Extensions.Tests/StringExtensionsTests.cs @@ -0,0 +1,109 @@ +using PowerCSharp.Extensions.Strings; +using Xunit; + +namespace PowerCSharp.Extensions.Tests; + +public class StringExtensionsTests +{ + #region Coalesce Tests + + [Fact] + public void Coalesce_WithNonNullRawValue_ShouldReturnRawValue() + { + // Arrange + string rawValue = "hello"; + string fallback = "fallback"; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Equal(rawValue, result); + } + + [Fact] + public void Coalesce_WithEmptyRawValue_ShouldReturnFallback() + { + // Arrange + string rawValue = ""; + string fallback = "fallback"; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Equal(fallback, result); + } + + [Fact] + public void Coalesce_WithNullRawValue_ShouldReturnFallback() + { + // Arrange + string? rawValue = null; + string fallback = "fallback"; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Equal(fallback, result); + } + + [Fact] + public void Coalesce_WithWhitespaceRawValue_ShouldReturnRawValue() + { + // Arrange + string rawValue = " "; + string fallback = "fallback"; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Equal(rawValue, result); + } + + [Fact] + public void Coalesce_WithNullFallback_ShouldReturnFallback() + { + // Arrange + string rawValue = ""; + string? fallback = null; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Coalesce_WithBothNull_ShouldReturnNull() + { + // Arrange + string? rawValue = null; + string? fallback = null; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Null(result); + } + + [Fact] + public void Coalesce_WithBothEmpty_ShouldReturnFallback() + { + // Arrange + string rawValue = ""; + string fallback = ""; + + // Act + var result = rawValue.Coalesce(fallback); + + // Assert + Assert.Equal(fallback, result); + } + + #endregion +} diff --git a/tests/PowerCSharp.Features.Tests/PowerCSharp.Features.Tests.csproj b/tests/PowerCSharp.Features.Tests/PowerCSharp.Features.Tests.csproj index 8972f8f..3b4e1d1 100644 --- a/tests/PowerCSharp.Features.Tests/PowerCSharp.Features.Tests.csproj +++ b/tests/PowerCSharp.Features.Tests/PowerCSharp.Features.Tests.csproj @@ -20,9 +20,9 @@ - - - + + + diff --git a/tests/PowerCSharp.Utilities.Tests/MathHelperTests.cs b/tests/PowerCSharp.Utilities.Tests/MathHelperTests.cs index 308770b..d8e930b 100644 --- a/tests/PowerCSharp.Utilities.Tests/MathHelperTests.cs +++ b/tests/PowerCSharp.Utilities.Tests/MathHelperTests.cs @@ -14,7 +14,7 @@ public void Clamp_ShouldReturnMinWhenValueIsBelowMin() int max = 20; // Act - int result = MathHelper.Clamp(value, min, max); + int result = MathUtility.Clamp(value, min, max); // Assert Assert.Equal(min, result); @@ -29,7 +29,7 @@ public void Clamp_ShouldReturnMaxWhenValueIsAboveMax() int max = 20; // Act - int result = MathHelper.Clamp(value, min, max); + int result = MathUtility.Clamp(value, min, max); // Assert Assert.Equal(max, result); @@ -44,7 +44,7 @@ public void Clamp_ShouldReturnValueWhenInRange() int max = 20; // Act - int result = MathHelper.Clamp(value, min, max); + int result = MathUtility.Clamp(value, min, max); // Assert Assert.Equal(value, result); @@ -59,7 +59,7 @@ public void IsInRange_ShouldReturnTrueWhenValueInRange() int max = 20; // Act - bool result = MathHelper.IsInRange(value, min, max); + bool result = MathUtility.IsInRange(value, min, max); // Assert Assert.True(result); @@ -74,7 +74,7 @@ public void IsInRange_ShouldReturnTrueWhenValueEqualsMin() int max = 20; // Act - bool result = MathHelper.IsInRange(value, min, max); + bool result = MathUtility.IsInRange(value, min, max); // Assert Assert.True(result); @@ -89,7 +89,7 @@ public void IsInRange_ShouldReturnTrueWhenValueEqualsMax() int max = 20; // Act - bool result = MathHelper.IsInRange(value, min, max); + bool result = MathUtility.IsInRange(value, min, max); // Assert Assert.True(result); @@ -104,7 +104,7 @@ public void IsInRange_ShouldReturnFalseWhenValueBelowMin() int max = 20; // Act - bool result = MathHelper.IsInRange(value, min, max); + bool result = MathUtility.IsInRange(value, min, max); // Assert Assert.False(result); @@ -119,7 +119,7 @@ public void IsInRange_ShouldReturnFalseWhenValueAboveMax() int max = 20; // Act - bool result = MathHelper.IsInRange(value, min, max); + bool result = MathUtility.IsInRange(value, min, max); // Assert Assert.False(result); @@ -133,7 +133,7 @@ public void Percentage_ShouldCalculateCorrectPercentage() double total = 100; // Act - double result = MathHelper.Percentage(part, total); + double result = MathUtility.Percentage(part, total); // Assert Assert.Equal(25.0, result); @@ -147,7 +147,7 @@ public void Percentage_ShouldReturnZeroWhenTotalIsZero() double total = 0; // Act - double result = MathHelper.Percentage(part, total); + double result = MathUtility.Percentage(part, total); // Assert Assert.Equal(0.0, result); @@ -160,7 +160,7 @@ public void ToRadians_ShouldConvertDegreesToRadians() double degrees = 180; // Act - double result = MathHelper.ToRadians(degrees); + double result = MathUtility.ToRadians(degrees); // Assert Assert.Equal(Math.PI, result, 5); @@ -173,7 +173,7 @@ public void ToDegrees_ShouldConvertRadiansToDegrees() double radians = Math.PI; // Act - double result = MathHelper.ToDegrees(radians); + double result = MathUtility.ToDegrees(radians); // Assert Assert.Equal(180.0, result, 5); @@ -186,7 +186,7 @@ public void IsEven_ShouldReturnTrueForEvenNumber() int number = 4; // Act - bool result = MathHelper.IsEven(number); + bool result = MathUtility.IsEven(number); // Assert Assert.True(result); @@ -199,7 +199,7 @@ public void IsEven_ShouldReturnFalseForOddNumber() int number = 3; // Act - bool result = MathHelper.IsEven(number); + bool result = MathUtility.IsEven(number); // Assert Assert.False(result); @@ -212,7 +212,7 @@ public void IsOdd_ShouldReturnTrueForOddNumber() int number = 3; // Act - bool result = MathHelper.IsOdd(number); + bool result = MathUtility.IsOdd(number); // Assert Assert.True(result); @@ -225,7 +225,7 @@ public void IsOdd_ShouldReturnFalseForEvenNumber() int number = 4; // Act - bool result = MathHelper.IsOdd(number); + bool result = MathUtility.IsOdd(number); // Assert Assert.False(result); diff --git a/tests/PowerCSharp.Utilities.Tests/PowerCSharp.Utilities.Tests.csproj b/tests/PowerCSharp.Utilities.Tests/PowerCSharp.Utilities.Tests.csproj index a5ee7d8..9b49ac0 100644 --- a/tests/PowerCSharp.Utilities.Tests/PowerCSharp.Utilities.Tests.csproj +++ b/tests/PowerCSharp.Utilities.Tests/PowerCSharp.Utilities.Tests.csproj @@ -20,8 +20,8 @@ - - + + diff --git a/tests/PowerCSharp.Utilities.Tests/UnitTest1.cs b/tests/PowerCSharp.Utilities.Tests/UnitTest1.cs deleted file mode 100644 index 790fccf..0000000 --- a/tests/PowerCSharp.Utilities.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace PowerCSharp.Utilities.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/tests/PowerCSharp.Utilities.Tests/ValidationUtilityTests.cs b/tests/PowerCSharp.Utilities.Tests/ValidationUtilityTests.cs index 1b10792..ef4ad71 100644 --- a/tests/PowerCSharp.Utilities.Tests/ValidationUtilityTests.cs +++ b/tests/PowerCSharp.Utilities.Tests/ValidationUtilityTests.cs @@ -90,7 +90,7 @@ public void IsNumeric_ShouldReturnFalseForNonNumericString() string text = "abc123"; // Act - bool result = ValidationHelper.IsNumeric(text); + bool result = ValidationUtility.IsNumeric(text); // Assert Assert.False(result); @@ -103,7 +103,7 @@ public void IsNumeric_ShouldReturnFalseForNullString() string? value = null; // Act - bool result = ValidationHelper.IsNumeric(value); + bool result = ValidationUtility.IsNumeric(value); // Assert Assert.False(result); @@ -116,7 +116,7 @@ public void IsNumeric_ShouldReturnFalseForEmptyString() string value = ""; // Act - bool result = ValidationHelper.IsNumeric(value); + bool result = ValidationUtility.IsNumeric(value); // Assert Assert.False(result);