In my program, I have a tree of Group
nodes, each identified by their ImportId
.
Each group has zero or more members, that I want to collect.
Each group has zero or more child groups, referenced by their importId, whose members should be added to the collection.
Each group has zero or more RulePart
s.
Each rulePart is a collection of zero or more groups, referenced by their importId, whose members should be added or removed to the collection, depending on a boolean value Negated
on each rulePart.
I got the following code working:
internal class RulePartTraverser
{
public List<string> GetRulePartMembers(IReadOnlyList<RulePart> ruleParts, IReadOnlyList<Group> allGroups)
{
if (ruleParts is null || !ruleParts.Any() || allGroups is null || !allGroups.Any())
{
return new List<string>();
}
var positiveRuleParts = collectRulePartMembers(ruleParts.Where(rp => !rp.Negated), allGroups);
var negativeRuleParts = collectRulePartMembers(ruleParts.Where(rp => rp.Negated), allGroups);
return positiveRuleParts.Except(negativeRuleParts).ToList();
}
private IEnumerable<string> collectRulePartMembers(IEnumerable<RulePart> ruleParts, IReadOnlyList<Group> allGroups)
{
var members = ruleParts.Aggregate(
seed: new List<string>().AsEnumerable(),
(current, rp) => current.Union(collectIndirectMembers(rp.RulePartMemberImportIds, allGroups)));
return members;
}
private IEnumerable<string> collectIndirectMembers(List<string> indirectGroupImportIds, IReadOnlyList<Group> allGroups)
{
var returnList = indirectGroupImportIds.Aggregate(
seed: new List<string>().AsEnumerable(),
(current, importId) =>
{
var correspondingGroup = allGroups.Single(ag => ag.ImportId == importId);
return current.Union(GetRulePartMembers(correspondingGroup.RuleParts, allGroups))
.Union(collectIndirectMembers(correspondingGroup.Children, allGroups))
.Union(correspondingGroup.Members);
});
return returnList;
}
}
public class RulePart
{
public bool Negated { get; set; }
public List<string> RulePartMemberImportIds { get; set; }
}
public class Group
{
public string ImportId { get; set; }
public string Name { get; set; }
public List<string> Children { get; set; }
public List<string> Members { get; set; }
public List<RulePart> RuleParts { get; set; }
}
The above method accepts a list of ruleParts for a given group and a list of all groups.
Some sanity checks are performed and if they fail, an empty list is returned.
It then collects all members of all ruleParts that are not negated, then collects all members of all ruleParts that are negated and substracts the latter from the former. The resulting collection is returned.
In order to collect all members of a single rulePart, all rulePartMembers are inspected and all the members of the corresponding groups are collected.
To collect all members of a given single group, I recursively collect all members of the corresponding ruleParts; then I recursively add the members of all child groups, potentially collecting the members of this child group's ruleParts and this child group's children. Finally I add all direct members of the group and step up.
Due to the nature of my data structure, I fear that this is the best I can do. However, is it possible (or sensible) to replace the recursion by an iteration? Are there other things I could or should improve?
The following example is not too far away from a real case scenario, I hope it helps in explaining the data structure:
var rootGroup = new Group
{
ImportId = "Root",
Members = new List<string>(),
Children = new List<string>(),
RuleParts = new List<RulePart>
{
new RulePart{Negated = false, RulePartMemberImportIds = new List<string>{ "group01", "group02" } }, //add all members of groups group01 and group02 to the collection
new RulePart{Negated = true, RulePartMemberImportIds = new List<string>{"group03", "group04"}} //but remove all members of groups group03 and group04
}
};
var group01 = new Group
{
ImportId = "group01",
Members = new List<string>(),
Children = new List<string>(),
RuleParts = new List<RulePart>
{
new RulePart{Negated = false, RulePartMemberImportIds = new List<string>{"group05"}}, //add all members of group05
new RulePart{Negated = true, RulePartMemberImportIds = new List<string>{"group06"}} //remove all members of group06
}
};
var group02 = new Group
{
ImportId = "group02",
Members = new List<string> { "member01", "member04", "member05" },
Children = new List<string>(),
RuleParts = new List<RulePart>()
};
var group03 = new Group
{
ImportId = "group03",
Members = new List<string> { "member06", "member07", "member09" },
Children = new List<string>(),
RuleParts = new List<RulePart>()
};
var group04 = new Group
{
ImportId = "group04",
Members = new List<string> { "member09", "member10", "member11" },
Children = new List<string>(),
RuleParts = new List<RulePart>()
};
var group05 = new Group
{
ImportId = "group05",
Members = new List<string>(),
Children = new List<string> { "group07", "group08" }, //add all members of groups group07 and group08
RuleParts = new List<RulePart>()
};
var group06 = new Group
{
ImportId = "group06",
Members = new List<string> { "member02" },
Children = new List<string>(),
RuleParts = new List<RulePart>()
};
var group07 = new Group
{
ImportId = "group07",
Members = new List<string> { "member12" },
Children = new List<string>(),
RuleParts = new List<RulePart>()
};
var group08 = new Group
{
ImportId = "group08",
Members = new List<string>(),
Children = new List<string> { "group04" }, //add all members of group04
RuleParts = new List<RulePart>()
};
var expectedMembers = new List<string> { "member01", "member04", "member05", "member12" };
The rule parts of the Root
Group would serve as input to GetRulePartMembers
.
In order to collect the members of the different rule parts, first the members of groups group01
- group04
have to be determined.
For group01
, this means determining the members of the ruleparts of group01
, ie determining the members of groups group05
and group06
, as these are ruleparts of group01
.
As group05
has child groups, the members of group05
is the union of the members of groups group07
and group08
, group08
has a single child group group04
, so the members of group08
are the members of group04
, yielding member09, member10, member11
for group08
, resulting in member09, member10, member11, member12
for group05
.
The single member of group06
is member02
, removing it from the result collection of group05
results in the collection member09, member10, member11, member12
for group01
.
For group02
, the members are directly member01, member04, member05
, which results in the member collection member01, member04, member05, member09, member10, member11, member12
for the non-negated rulepart of the Root
Group.
For the negated rulepart of the Root
Group, the members of group03
and group04
can directly be added to the result collection, as neither of the groups have children or ruleparts. This results in member06, member07, member09, member10, member11
for the negated rulepart.
These members are then removed from the collection of members for the non-negated rulepart, yielding the final result of member01, member04, member05, member12
for the Root
Group.