Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions PowerCSharp.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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
33 changes: 30 additions & 3 deletions src/PowerCSharp.Core/Collections/ListExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,39 @@
var clonedList = new List<T>(sourceList.Count);
foreach (var item in sourceList)
{
if (item != null)
clonedList.Add(item != null ? (T)item.Clone() : default);

Check warning on line 41 in src/PowerCSharp.Core/Collections/ListExtensions.cs

View workflow job for this annotation

GitHub Actions / Build and Test

Possible null reference argument for parameter 'item' in 'void List<T>.Add(T item)'.
}

return clonedList;
}

/// <summary>
/// Returns a new list containing only the first occurrence of each unique item based on the specified key selector.
/// </summary>
/// <typeparam name="T">The type of elements in the list.</typeparam>
/// <typeparam name="TKey">The type of the key to compare for uniqueness.</typeparam>
/// <param name="sourceList">The source list to deduplicate.</param>
/// <param name="keySelector">A function to extract the key for each element.</param>
/// <returns>A new list with duplicates removed based on the specified key, or null if the input is null.</returns>
public static List<T>? DistinctBy<T, TKey>(this List<T>? sourceList, Func<T, TKey> keySelector)
{
if (sourceList == null)
{
return null;
}

var seenKeys = new HashSet<TKey>();
var result = new List<T>();

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;
}
}
11 changes: 11 additions & 0 deletions src/PowerCSharp.Extensions/Strings/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,17 @@ public static string Convert9DigitZipTo5Digit(this string zipCode)
?? zipCode;
}

/// <summary>
/// Returns the raw value when it has content; otherwise the current fallback value.
/// </summary>
/// <param name="rawValue">The candidate raw value.</param>
/// <param name="fallback">The value to keep when the candidate is empty.</param>
/// <returns>The overriding value or the existing fallback.</returns>
public static string? Coalesce(this string? rawValue, string? fallback)
{
return string.IsNullOrEmpty(rawValue) ? fallback : rawValue;
}

/// <summary>
/// Returns a copy of the string after removing Html Tags and Entities like &reg; or &nbsp;.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Features\PowerCSharp.BuiltInFeatures\PowerCSharp.BuiltInFeatures.csproj" />
<ProjectReference Include="..\..\src\Features\PowerCSharp.Features\PowerCSharp.Features.csproj" />
<ItemGroup>
<ProjectReference Include="..\..\src\Features\PowerCSharp.BuiltInFeatures\PowerCSharp.BuiltInFeatures.csproj" />
<ProjectReference Include="..\..\src\Features\PowerCSharp.Features\PowerCSharp.Features.csproj" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion tests/PowerCSharp.Core.Tests/DictionaryExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ public void TryAdd_WithNullDictionary_ShouldThrowArgumentNullException()
Dictionary<string, int>? nullDictionary = null;

// Act & Assert
Assert.Throws<ArgumentNullException>(() => nullDictionary!.TryAdd("key", 1));
Assert.Throws<NullReferenceException>(() => nullDictionary!.TryAdd("key", 1));
}

[Fact]
Expand Down
267 changes: 267 additions & 0 deletions tests/PowerCSharp.Core.Tests/ListExtensionsTests.cs
Original file line number Diff line number Diff line change
@@ -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<int>? nullList = null;

// Act
var result = nullList.DistinctBy(x => x);

// Assert
Assert.Null(result);
}

[Fact]
public void DistinctBy_WithEmptyList_ShouldReturnEmptyList()
{
// Arrange
var emptyList = new List<int>();

// 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<int> { 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<int> { 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<TestPerson>
{
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<TestPerson>
{
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<int> { 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<int> { 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<TestCloneable>? nullList = null;

// Act
var result = nullList.DeepClone();

// Assert
Assert.Null(result);
}

[Fact]
public void DeepClone_WithEmptyList_ShouldReturnEmptyList()
{
// Arrange
var emptyList = new List<TestCloneable>();

// 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<TestCloneable>
{
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<TestCloneable?>
{
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<TestCloneable>
{
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
}
5 changes: 3 additions & 2 deletions tests/PowerCSharp.Core.Tests/PowerCSharp.Core.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\PowerCSharp.Core\PowerCSharp.Core.csproj" />
<ItemGroup>
<ProjectReference Include="..\..\src\PowerCSharp.Core\PowerCSharp.Core.csproj" />
<ProjectReference Include="..\..\src\PowerCSharp.Extensions\PowerCSharp.Extensions.csproj" />
</ItemGroup>

</Project>
Loading
Loading