Reviewing Mendz.Graphs.Edge

The Graph Theory in C# presented the Graph, Vertex and Edge classes to implement the graph theory's definition of a graph: G = (V, E). While reviewing Mendz.Graphs for memory and performance optimization, it went through dramatic changes and enhancements. Previously, I reviewed the Vertex. This article focuses on the Edge.

The Edge has so many changes and enhancements, they deserve to be listed.
  1. I find it necessary to further enforce the graph definition rule which states that the edge's endpoints should be vertices that exist in Graph.Vertices. In order to achieve this, the Edge is changed into a sealed class. This will prevent anyone from extending the Edge in to a version that can break the rule. As a consolation, I decided to add a dynamic Expansion property, which will allow developers to dynamically add attributes to their edges.
  2. I replaced the Edge.ID to return a tuple (int tail, int directed, int head) and removed the nested class IDGenerator. The ID as a tuple makes more sense specially when indexing the graph. This feature requires the System.ValueTuple NuGet package. To make sure that I have only one way to build the ID, I created the SetID() method.
  3. As a consequence of replacing the ID property into a tuple, I also updated the Equals() and GetHashCode() overrides to use Edge.ID and to recognize that it's a tuple.
  4. I changed the Endpoint property's type from Vertex[] to Tuple<Vertex, Vertex>. I used Tuple instead of ValueTuple because Tuple's Items are read-only.
  5. Edge.Weight is changed from type int to double.
  6. I changed the ToString() override implementation to use the vertex IDs instead of the vertex values when generating the DOT notation. As a consolation, I created the ToStringByValue() method, which returns the DOT notation using the vertex values.
  7. I added methods to assist in working with the new Edge.ID and its parts. This includes implementing the Deconstruct() method and the static translation methods like ToDirectedIndicator(), ToDirectedDOTNotation(), NameToID() and IDToName().
  8. The new Edge class also inherits IComparable, which is implemented to compare by Edge.ID. This allows collections of Edge instances (ex. Edge[] _indexedEdges;) to be sorted directly (ex. Array.Sort(_indexedEdges);) without having to extract or store copies of the edge IDs.

Edge.cs
using System;
using System.Text.RegularExpressions;

namespace Mendz.Graphs
{
    public sealed class Edge : IComparable<Edge>
    {
        public const string DIRECTED_DOT_NOTATION = " -> ";
        public const string UNDIRECTED_DOT_NOTATION = " -- ";

        private Tuple<Vertex, Vertex> _endpoints;

        private (int tail, int directed, int head) _id;
        public (int tail, int directed, int head) ID
        {
            get
            {
                return _id;
            }
        }

        public string Name
        {
            get
            {
                return ToString();
            }
        }

        public string Label { get; set; }

        public Tuple<Vertex, Vertex> EndPoints
        {
            get
            {
                return _endpoints;
            }
        }

        public Vertex Tail
        {
            get
            {
                return _endpoints.Item1;
            }
        }

        public Vertex Head
        {
            get
            {
                return _endpoints.Item2;
            }
        }

        private bool _directed;
        public bool Directed
        {
            get
            {
                return _directed;
            }
            set
            {
                if (_directed != value)
                {
                    _directed = value;
                    SetID();
                }
            }
        }

        public double Weight { get; set; }

        public dynamic Expansion { get; set; }

        private Edge(Vertex tail, Vertex head, 
            bool directed = false, double weight = 0, string label = "")
        {
            _endpoints = new Tuple<Vertex, Vertex>(tail, head);
            _directed = directed;
            Weight = weight;
            Label = label;
            SetID();
        }

        public string ToStringByValue()
        {
            return ToString(_endpoints.Item1.Value.ToString(), 
                _endpoints.Item2.Value.ToString());
        }

        public void Deconstruct(out int tail, out int directed, out int head)
        {
            tail = _endpoints.Item1.ID;
            directed = ToDirectedIndicator(_directed);
            head = _endpoints.Item2.ID;
        }

        private void SetID()
        {
            (int tail, int directed, int head) = this;
            _id = (tail, directed, head);
        }

        private string ToString(string tail, string head)
        {
            return tail + Edge.ToDirectedDOTNotation(_directed) + head;
        }

        #region Overrides
        public override bool Equals(object obj)
        {
            if (obj == null || !(obj is Edge))
            {
                return false;
            }
            return _id.Equals(((Edge)obj).ID);
        }

        public override int GetHashCode()
        {
            return _id.GetHashCode();
        }

        public override string ToString()
        {
            return ToString(_endpoints.Item1.ID.ToString(), 
                _endpoints.Item2.ID.ToString());
        }
        #endregion

        #region Implements
        public int CompareTo(Edge other)
        {
            return ID.CompareTo(other.ID);
        }
        #endregion

        public static int ToDirectedIndicator(bool directed)
        {
            return (directed ? 1 : 0);
        }

        public static int ToDirectedIndicator(string directedDOTNotation)
        {
            if (directedDOTNotation.Trim() == DIRECTED_DOT_NOTATION.Trim())
            {
                return ToDirectedIndicator(true);
            }
            else if (directedDOTNotation.Trim() == UNDIRECTED_DOT_NOTATION.Trim())
            {
                return ToDirectedIndicator(false);
            }
            else
            {
                throw new ArgumentOutOfRangeException("directedDOTNotation", 
                    "Only Edge.DIRECTED_DOT_NOTATION value or " +
                    "Edge.UNDIRECTED_DOT_NOTATION value can be recognized.");
            }
        }

        public static string ToDirectedDOTNotation(bool directed)
        {
            return (directed ? DIRECTED_DOT_NOTATION : UNDIRECTED_DOT_NOTATION);
        }

        public static string ToDirectedDOTNotation(int directed)
        {
            if (directed == 1 || directed == 0)
            {
                return ToDirectedDOTNotation((directed == 1 ? true : false));
            }
            else
            {
                throw new ArgumentOutOfRangeException("directed", 
                    "Only 1 or 0 can be recognized.");
            }
        }

        public static (int tail, int directed, int head) NameToID(string edgeName)
        {
            if (!Regex.IsMatch(edgeName, @"^\d+ -[->] \d+$"))
            {
                throw new ArgumentException("Invalid edge name.", "edgeName");
            }
            string[] id = edgeName.Split(' ');
            return (Convert.ToInt32(id[0]), 
                ToDirectedIndicator(id[1]), 
                Convert.ToInt32(id[2]));
        }

        public static string IDToName((int tail, int directed, int head) edgeID)
        {
            if (edgeID.tail <= 0 || 
                edgeID.directed < 0 || edgeID.directed > 1 || 
                edgeID.head <= 0)
            {
                throw new ArgumentException("Invalid edge ID.", "edgeID");
            }
            return edgeID.tail.ToString() + 
                ToDirectedDOTNotation(edgeID.directed) + 
                edgeID.head.ToString();
        }

        internal static Edge Create(Vertex tail, Vertex head, 
            bool directed = false, double weight = 0, string label = "")
        {
            return new Edge(tail, head, directed, weight, label);
        }
    }
}

Update: Applied change to Edge.Weight.

Comments