Skip to content

Commit 2785e0c

Browse files
committed
ParentChildSorter
1 parent 871322c commit 2785e0c

3 files changed

Lines changed: 220 additions & 3 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using oledid.SyntaxImprovement.Algorithms;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
using Xunit;
6+
7+
namespace oledid.SyntaxImprovement.Tests.Algorithms
8+
{
9+
public class ParentChildSorterTests
10+
{
11+
[Fact]
12+
public void TestWithStringIds()
13+
{
14+
var groups = new List<StringGroup>
15+
{
16+
// Level 0
17+
new StringGroup { Id = "A", ParentId = null },
18+
19+
// Level 1
20+
new StringGroup { Id = "B", ParentId = "A" },
21+
new StringGroup { Id = "C", ParentId = "A" },
22+
23+
// Level 2
24+
new StringGroup { Id = "D", ParentId = "B" },
25+
new StringGroup { Id = "E", ParentId = "B" },
26+
new StringGroup { Id = "F", ParentId = "C" },
27+
new StringGroup { Id = "G", ParentId = "C" },
28+
29+
// Level 3
30+
new StringGroup { Id = "H", ParentId = "D" },
31+
new StringGroup { Id = "I", ParentId = "E" },
32+
new StringGroup { Id = "J", ParentId = "F" },
33+
new StringGroup { Id = "K", ParentId = "G" },
34+
35+
// Level 4
36+
new StringGroup { Id = "L", ParentId = "H" },
37+
new StringGroup { Id = "M", ParentId = "I" },
38+
new StringGroup { Id = "N", ParentId = "J" },
39+
new StringGroup { Id = "O", ParentId = "K" }
40+
};
41+
42+
// Shuffle the list to simulate unordered input
43+
var random = new Random();
44+
groups = groups.OrderBy(_ => random.Next()).ToList();
45+
46+
var orderedGroups = ParentChildSorter.SortGroupsByParentChildRelationship(
47+
groups,
48+
e => e.ParentId,
49+
e => e.Id);
50+
51+
var expectedOrder = new List<string>
52+
{
53+
"A", // Level 0
54+
"B", "C", // Level 1
55+
"D", "E", "F", "G", // Level 2
56+
"H", "I", "J", "K", // Level 3
57+
"L", "M", "N", "O" // Level 4
58+
};
59+
60+
// Assert that the orderedGroups match the expected order
61+
Assert.Equal(expectedOrder.Count, orderedGroups.Count);
62+
for (int i = 0; i < expectedOrder.Count; i++)
63+
{
64+
Assert.Equal(expectedOrder[i], orderedGroups[i].Id);
65+
}
66+
}
67+
68+
[Fact]
69+
public void TestWithLongIds()
70+
{
71+
// Define a list of groups with long IDs
72+
var groups = new List<LongGroup>
73+
{
74+
// Level 0
75+
new LongGroup { Id = 1, ParentId = null },
76+
77+
// Level 1
78+
new LongGroup { Id = 2, ParentId = 1 },
79+
new LongGroup { Id = 3, ParentId = 1 },
80+
81+
// Level 2
82+
new LongGroup { Id = 4, ParentId = 2 },
83+
new LongGroup { Id = 5, ParentId = 2 },
84+
new LongGroup { Id = 6, ParentId = 3 },
85+
new LongGroup { Id = 7, ParentId = 3 },
86+
87+
// Level 3
88+
new LongGroup { Id = 8, ParentId = 4 },
89+
new LongGroup { Id = 9, ParentId = 5 },
90+
new LongGroup { Id = 10, ParentId = 6 },
91+
new LongGroup { Id = 11, ParentId = 7 },
92+
93+
// Level 4
94+
new LongGroup { Id = 12, ParentId = 8 },
95+
new LongGroup { Id = 13, ParentId = 9 },
96+
new LongGroup { Id = 14, ParentId = 10 },
97+
new LongGroup { Id = 15, ParentId = 11 }
98+
};
99+
100+
// Shuffle the list to simulate unordered input
101+
var random = new Random();
102+
groups = groups.OrderBy(_ => random.Next()).ToList();
103+
104+
var orderedGroups = ParentChildSorter.SortGroupsByParentChildRelationship(
105+
groups,
106+
e => e.ParentId,
107+
e => e.Id);
108+
109+
var expectedOrder = new List<long>
110+
{
111+
1, // Level 0
112+
2, 3, // Level 1
113+
4, 5, 6, 7, // Level 2
114+
8, 9, 10, 11, // Level 3
115+
12, 13, 14, 15 // Level 4
116+
};
117+
118+
// Assert that the orderedGroups match the expected order
119+
Assert.Equal(expectedOrder.Count, orderedGroups.Count);
120+
for (int i = 0; i < expectedOrder.Count; i++)
121+
{
122+
Assert.Equal(expectedOrder[i], orderedGroups[i].Id);
123+
}
124+
}
125+
126+
public class StringGroup
127+
{
128+
public string Id { get; set; }
129+
public string? ParentId { get; set; }
130+
}
131+
132+
public class LongGroup
133+
{
134+
public long Id { get; set; }
135+
public long? ParentId { get; set; }
136+
}
137+
}
138+
}

src/SyntaxImprovement.Tests/SyntaxImprovement.Tests.csproj

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
</PropertyGroup>
1212

1313
<ItemGroup>
14-
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.0" />
15-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
16-
<PackageReference Include="xunit" Version="2.9.0" />
14+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="8.0.1" />
15+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
16+
<PackageReference Include="xunit" Version="2.9.2" />
1717
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
1818
<PrivateAssets>all</PrivateAssets>
1919
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace oledid.SyntaxImprovement.Algorithms
6+
{
7+
public static class ParentChildSorter
8+
{
9+
/// <summary>
10+
/// Sorts a list of items based on their parent-child relationships using breadth-first traversal.
11+
///
12+
/// This method ensures that:
13+
/// - Parents come before their immediate children.
14+
/// - Siblings (items with the same parent) are ordered based on a specified key (typically the item's ID).
15+
/// - The traversal proceeds level by level, processing all nodes at the current depth before moving to the next level.
16+
///
17+
/// This is particularly useful when inserting items into a database with foreign key constraints,
18+
/// as it ensures that parent items are inserted before their children to avoid constraint violations.
19+
///
20+
/// Algorithm:
21+
/// 1. Create a lookup to map parent IDs to their immediate children.
22+
/// 2. Initialize a queue for breadth-first traversal.
23+
/// 3. Start with root nodes (items with null ParentId), ordered by the key selector.
24+
/// 4. While the queue is not empty:
25+
/// a. Dequeue an item and add it to the sorted list.
26+
/// b. Enqueue the item's children (ordered by the key selector) if they haven't been visited.
27+
/// 5. Return the sorted list.
28+
///
29+
/// Parameters:
30+
/// <param name="groups">The list of items to sort.</param>
31+
/// <param name="getParentId">Function to retrieve the ParentId of an item.</param>
32+
/// <param name="getId">Function to retrieve the Id of an item.</param>
33+
///
34+
/// Returns:
35+
/// <returns>A list of items sorted based on their parent-child relationships.</returns>
36+
/// </summary>
37+
public static List<T> SortGroupsByParentChildRelationship<T, TKey>(
38+
List<T> groups,
39+
Func<T, TKey?> getParentId,
40+
Func<T, TKey> getId) where TKey : notnull
41+
{
42+
// Create a lookup to map ParentId to its immediate children
43+
var lookup = groups.ToLookup(getParentId);
44+
45+
var sortedList = new List<T>();
46+
var visited = new HashSet<TKey?>();
47+
48+
var queue = new Queue<T>();
49+
50+
// Start with root nodes (ParentId == null), ordered by ID
51+
var rootNodes = lookup[default].OrderBy(getId);
52+
foreach (var rootNode in rootNodes)
53+
{
54+
queue.Enqueue(rootNode);
55+
visited.Add(getId(rootNode));
56+
}
57+
58+
// Perform breadth-first traversal
59+
while (queue.Count > 0)
60+
{
61+
var currentNode = queue.Dequeue();
62+
sortedList.Add(currentNode);
63+
var currentId = getId(currentNode);
64+
65+
// Get children of the current node, ordered by ID
66+
var children = lookup[currentId]
67+
.Where(child => visited.Add(getId(child))) // Add to visited if not already present
68+
.OrderBy(getId);
69+
70+
foreach (var child in children)
71+
{
72+
queue.Enqueue(child);
73+
}
74+
}
75+
76+
return sortedList;
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)