Files
Umbraco-CMS/src/Umbraco.Core/Collections/TopoGraph.cs
2022-01-13 09:27:37 +01:00

135 lines
5.0 KiB
C#

using System;
using System.Collections.Generic;
namespace Umbraco.Cms.Core.Collections
{
public class TopoGraph
{
internal const string CycleDependencyError = "Cyclic dependency.";
internal const string MissingDependencyError = "Missing dependency.";
public class Node<TKey, TItem>
{
public Node(TKey key, TItem item, IEnumerable<TKey> dependencies)
{
Key = key;
Item = item;
Dependencies = dependencies;
}
public TKey Key { get; }
public TItem Item { get; }
public IEnumerable<TKey> Dependencies { get; }
}
public static Node<TKey, TItem> CreateNode<TKey, TItem>(TKey key, TItem item, IEnumerable<TKey> dependencies)
=> new Node<TKey, TItem>(key, item, dependencies);
}
/// <summary>
/// Represents a generic DAG that can be topologically sorted.
/// </summary>
/// <typeparam name="TKey">The type of the keys.</typeparam>
/// <typeparam name="TItem">The type of the items.</typeparam>
public class TopoGraph<TKey, TItem> : TopoGraph
where TKey : notnull
{
private readonly Func<TItem, TKey> _getKey;
private readonly Func<TItem, IEnumerable<TKey>?> _getDependencies;
private readonly Dictionary<TKey, TItem> _items = new Dictionary<TKey, TItem>();
/// <summary>
/// Initializes a new instance of the <see cref="TopoGraph{TKey, TItem}"/> class.
/// </summary>
/// <param name="getKey">A method that returns the key of an item.</param>
/// <param name="getDependencies">A method that returns the dependency keys of an item.</param>
public TopoGraph(Func<TItem, TKey> getKey, Func<TItem, IEnumerable<TKey>?> getDependencies)
{
_getKey = getKey;
_getDependencies = getDependencies;
}
/// <summary>
/// Adds an item to the graph.
/// </summary>
/// <param name="item">The item.</param>
public void AddItem(TItem item)
{
var key = _getKey(item);
_items[key] = item;
}
/// <summary>
/// Adds items to the graph.
/// </summary>
/// <param name="items">The items.</param>
public void AddItems(IEnumerable<TItem> items)
{
foreach (var item in items)
AddItem(item);
}
/// <summary>
/// Gets the sorted items.
/// </summary>
/// <param name="throwOnCycle">A value indicating whether to throw on cycles, or just ignore the branch.</param>
/// <param name="throwOnMissing">A value indicating whether to throw on missing dependency, or just ignore the dependency.</param>
/// <param name="reverse">A value indicating whether to reverse the order.</param>
/// <returns>The (topologically) sorted items.</returns>
public IEnumerable<TItem> GetSortedItems(bool throwOnCycle = true, bool throwOnMissing = true, bool reverse = false)
{
var sorted = new TItem[_items.Count];
var visited = new HashSet<TItem>();
var index = reverse ? _items.Count - 1 : 0;
var incr = reverse ? -1 : +1;
foreach (var item in _items.Values)
Visit(item, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing);
return sorted;
}
private static bool Contains(TItem[] items, TItem item, int start, int count)
{
return Array.IndexOf(items, item, start, count) >= 0;
}
private void Visit(TItem item, ISet<TItem> visited, TItem[] sorted, ref int index, int incr, bool throwOnCycle, bool throwOnMissing)
{
if (visited.Contains(item))
{
// visited but not sorted yet = cycle
var start = incr > 0 ? 0 : index;
var count = incr > 0 ? index : sorted.Length - index;
if (throwOnCycle && Contains(sorted, item, start, count) == false)
throw new Exception(CycleDependencyError +": " + item);
return;
}
visited.Add(item);
var keys = _getDependencies(item);
var dependencies = keys == null ? null : FindDependencies(keys, throwOnMissing);
if (dependencies != null)
foreach (var dep in dependencies)
Visit(dep, visited, sorted, ref index, incr, throwOnCycle, throwOnMissing);
sorted[index] = item;
index += incr;
}
private IEnumerable<TItem> FindDependencies(IEnumerable<TKey> keys, bool throwOnMissing)
{
foreach (var key in keys)
{
TItem? value;
if (_items.TryGetValue(key, out value))
yield return value;
else if (throwOnMissing)
throw new Exception($"{MissingDependencyError} Error in type {typeof(TItem).Name}, with key {key}");
}
}
}
}