From 5991385193ad306c2913c790679e7e48803840f3 Mon Sep 17 00:00:00 2001 From: ilya-krivtsov <180809461+ilya-krivtsov@users.noreply.github.com> Date: Thu, 27 Mar 2025 23:43:45 +0300 Subject: [PATCH 1/4] Added Routers project (hw5 - routers) (wip) --- Routers/Routers.Tests/Routers.Tests.csproj | 28 +++++++++++++ Routers/Routers.sln | 48 ++++++++++++++++++++++ Routers/Routers/Routers.csproj | 14 +++++++ 3 files changed, 90 insertions(+) create mode 100644 Routers/Routers.Tests/Routers.Tests.csproj create mode 100644 Routers/Routers.sln create mode 100644 Routers/Routers/Routers.csproj diff --git a/Routers/Routers.Tests/Routers.Tests.csproj b/Routers/Routers.Tests/Routers.Tests.csproj new file mode 100644 index 0000000..0713f82 --- /dev/null +++ b/Routers/Routers.Tests/Routers.Tests.csproj @@ -0,0 +1,28 @@ + + + + net9.0 + latest + enable + enable + + true + + + + + + + + + + + + + + + + + + + diff --git a/Routers/Routers.sln b/Routers/Routers.sln new file mode 100644 index 0000000..c3ca7aa --- /dev/null +++ b/Routers/Routers.sln @@ -0,0 +1,48 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers", "Routers\Routers.csproj", "{19D4C610-E237-45B9-A8D2-A52A9FCD9986}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers.Tests", "Routers.Tests\Routers.Tests.csproj", "{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|Any CPU.Build.0 = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x64.ActiveCfg = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x64.Build.0 = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x86.ActiveCfg = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Debug|x86.Build.0 = Debug|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|Any CPU.ActiveCfg = Release|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|Any CPU.Build.0 = Release|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x64.ActiveCfg = Release|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x64.Build.0 = Release|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x86.ActiveCfg = Release|Any CPU + {19D4C610-E237-45B9-A8D2-A52A9FCD9986}.Release|x86.Build.0 = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x64.ActiveCfg = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x64.Build.0 = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x86.ActiveCfg = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Debug|x86.Build.0 = Debug|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|Any CPU.Build.0 = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x64.ActiveCfg = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x64.Build.0 = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.ActiveCfg = Release|Any CPU + {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/Routers/Routers/Routers.csproj b/Routers/Routers/Routers.csproj new file mode 100644 index 0000000..59ee2e1 --- /dev/null +++ b/Routers/Routers/Routers.csproj @@ -0,0 +1,14 @@ + + + + Library + net9.0 + enable + enable + + + + + + + From a1a9ef27b6e6eb596f098cfca9d9ba200fe49bc6 Mon Sep 17 00:00:00 2001 From: ilya-krivtsov <180809461+ilya-krivtsov@users.noreply.github.com> Date: Fri, 28 Mar 2025 23:26:23 +0300 Subject: [PATCH 2/4] Added Graph and GraphOptimizer (hw5 - routers) (wip) --- Routers/Routers/Graph.cs | 160 ++++++++++++++++++++++++++++++ Routers/Routers/GraphOptimizer.cs | 63 ++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 Routers/Routers/Graph.cs create mode 100644 Routers/Routers/GraphOptimizer.cs diff --git a/Routers/Routers/Graph.cs b/Routers/Routers/Graph.cs new file mode 100644 index 0000000..ca0c2f1 --- /dev/null +++ b/Routers/Routers/Graph.cs @@ -0,0 +1,160 @@ +namespace Routers; + +using System.Text.RegularExpressions; + +/// +/// Network graph. +/// +public partial class Graph +{ + /// + /// Initializes a new instance of the class. + /// + /// Nodes of the graph. + internal Graph(IEnumerable nodes) + { + Nodes = [.. nodes]; + } + + /// + /// Gets all nodes in graph. + /// + internal List Nodes { get; } + + /// + /// Reads graph from . + /// + /// to read graph from. + /// Read graph. + public static Graph ReadGraph(TextReader reader) + { + var graph = new Graph([]); + var nodes = new Dictionary(); + + while (true) + { + var line = reader.ReadLine(); + if (line == null) + { + break; + } + + var nodeMatch = NodeRegex().Match(line); + if (!nodeMatch.Success || nodeMatch.Groups.Count != 3 || + !int.TryParse(nodeMatch.Groups[1].ValueSpan, out int nodeIndex)) + { + throw new InvalidDataException(); + } + + if (!nodes.TryGetValue(nodeIndex, out var node)) + { + node = new([]); + nodes[nodeIndex] = node; + graph.Nodes.Add(node); + } + + var rawConnections = nodeMatch.Groups[2].Value.Split(','); + var connections = new Dictionary(); + foreach (var connection in rawConnections) + { + var connectionMatch = ConnectionRegex().Match(connection); + if (!connectionMatch.Success || connectionMatch.Groups.Count != 3) + { + throw new InvalidDataException(); + } + + if (!int.TryParse(connectionMatch.Groups[1].ValueSpan, out int neighborIndex) || + !int.TryParse(connectionMatch.Groups[2].ValueSpan, out int bandwidth) || + connections.ContainsKey(neighborIndex)) + { + throw new InvalidDataException(); + } + + connections[neighborIndex] = bandwidth; + } + + foreach (var (neighborIndex, bandwidth) in connections) + { + if (!nodes.TryGetValue(neighborIndex, out var neighbor)) + { + neighbor = new([]); + nodes[neighborIndex] = neighbor; + graph.Nodes.Add(neighbor); + } + + node.Neighbors[neighbor] = bandwidth; + neighbor.Neighbors[node] = bandwidth; + } + } + + return graph; + } + + /// + /// Writes graph to . + /// + /// to write graph to. + public void Write(TextWriter writer) + { + var wroteConnections = new HashSet<(Node, Node)>(); + var nodeIndexLookup = new Dictionary(); + int lastIndex = 0; + + int GetIndex(Node node) + { + if (!nodeIndexLookup.TryGetValue(node, out int index)) + { + nodeIndexLookup[node] = lastIndex; + index = lastIndex; + lastIndex++; + } + + return index; + } + + foreach (var node in Nodes) + { + int neighborCount = 0; + foreach (var (neighbor, bandwidth) in node.Neighbors) + { + if (wroteConnections.Contains((node, neighbor)) || + wroteConnections.Contains((neighbor, node))) + { + continue; + } + + wroteConnections.Add((node, neighbor)); + wroteConnections.Add((neighbor, node)); + + if (neighborCount == 0) + { + writer.Write($"{GetIndex(node)}: "); + } + else if (neighborCount == 1) + { + writer.Write(", "); + } + + writer.Write($"{GetIndex(neighbor)} ({bandwidth})"); + neighborCount++; + } + + if (neighborCount != 0) + { + writer.WriteLine(); + } + } + } + + [GeneratedRegex(@"^\s*(\d+)\s*:(.*)$")] + private static partial Regex NodeRegex(); + + [GeneratedRegex(@"\s*(\d+)\s*\(\s*(\d+)\s*\)\s*")] + private static partial Regex ConnectionRegex(); + + /// + /// Graph node. + /// + /// Neighbors of node, stored as pairs of and bandwidth. + internal record Node(Dictionary Neighbors); +} diff --git a/Routers/Routers/GraphOptimizer.cs b/Routers/Routers/GraphOptimizer.cs new file mode 100644 index 0000000..26d080f --- /dev/null +++ b/Routers/Routers/GraphOptimizer.cs @@ -0,0 +1,63 @@ +namespace Routers; + +/// +/// Utility class for optimizing network graphs. +/// +public static class GraphOptimizer +{ + /// + /// Optimizes network graph. + /// + /// Graph to optimize. + /// Optimized graph. + public static Graph Optimize(Graph graph) + { + // use negative weights as workaround, so +1 is greater than any broadband + var broadbands = graph.Nodes.ToDictionary(x => x, x => 1); + var visited = new HashSet(); + + var queue = new PriorityQueue(); + queue.Enqueue(new(graph.Nodes[0], graph.Nodes[0]), 0); + + broadbands[graph.Nodes[0]] = 0; + + var newNodes = new List(); + var oldToNewMap = new Dictionary(); + while (queue.TryDequeue(out Edge edge, out int broadband)) + { + var node = edge.To; + visited.Add(node); + + var newNode = new Graph.Node([]); + newNodes.Add(newNode); + oldToNewMap[node] = newNode; + if (edge.From != edge.To) + { + oldToNewMap[edge.From].Neighbors[edge.To] = -broadband; + } + + foreach (var (neighbor, neighborBroadband) in node.Neighbors) + { + if (visited.Contains(neighbor)) + { + continue; + } + + if (neighborBroadband > broadbands[neighbor]) + { + broadbands[neighbor] = neighborBroadband; + queue.Enqueue(new(node, neighbor), -neighborBroadband); + } + } + } + + if (visited.Count != graph.Nodes.Count) + { + throw new ArgumentException("The graph is disconnected", nameof(graph)); + } + + return new(newNodes); + } + + private readonly record struct Edge(Graph.Node From, Graph.Node To); +} From 54da4a59c59deb5b0a485b18bbc2e7047eadb7b0 Mon Sep 17 00:00:00 2001 From: ilya-krivtsov <180809461+ilya-krivtsov@users.noreply.github.com> Date: Fri, 28 Mar 2025 23:44:37 +0300 Subject: [PATCH 3/4] Return bool instead of throwing exception (hw5 - routers) (wip) --- Routers/Routers/GraphOptimizer.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Routers/Routers/GraphOptimizer.cs b/Routers/Routers/GraphOptimizer.cs index 26d080f..785046a 100644 --- a/Routers/Routers/GraphOptimizer.cs +++ b/Routers/Routers/GraphOptimizer.cs @@ -1,5 +1,7 @@ namespace Routers; +using System.Diagnostics.CodeAnalysis; + /// /// Utility class for optimizing network graphs. /// @@ -9,8 +11,9 @@ public static class GraphOptimizer /// Optimizes network graph. /// /// Graph to optimize. - /// Optimized graph. - public static Graph Optimize(Graph graph) + /// When this method returns, contains optimized graph, if read successfully, otherwise. + /// if is connected, otherwise. + public static bool Optimize(Graph graph, [MaybeNullWhen(false)] out Graph optimized) { // use negative weights as workaround, so +1 is greater than any broadband var broadbands = graph.Nodes.ToDictionary(x => x, x => 1); @@ -53,10 +56,12 @@ public static Graph Optimize(Graph graph) if (visited.Count != graph.Nodes.Count) { - throw new ArgumentException("The graph is disconnected", nameof(graph)); + optimized = null; + return false; } - return new(newNodes); + optimized = new(newNodes); + return true; } private readonly record struct Edge(Graph.Node From, Graph.Node To); From cdd6cb4e126a6f43fc992eb8f5d229c3acfb2a42 Mon Sep 17 00:00:00 2001 From: ilya-krivtsov <180809461+ilya-krivtsov@users.noreply.github.com> Date: Fri, 28 Mar 2025 23:49:17 +0300 Subject: [PATCH 4/4] Added Routers.Cli (hw5 - routers) --- Routers/Routers.Cli/Program.cs | 62 ++++++++++++++++++++++++++ Routers/Routers.Cli/Routers.Cli.csproj | 14 ++++++ Routers/Routers.sln | 14 ++++++ 3 files changed, 90 insertions(+) create mode 100644 Routers/Routers.Cli/Program.cs create mode 100644 Routers/Routers.Cli/Routers.Cli.csproj diff --git a/Routers/Routers.Cli/Program.cs b/Routers/Routers.Cli/Program.cs new file mode 100644 index 0000000..cc86dfe --- /dev/null +++ b/Routers/Routers.Cli/Program.cs @@ -0,0 +1,62 @@ +using Routers; + +if (args.Length < 2) +{ + Console.WriteLine(""" + Network topology optimizer + Usage: + dotnet run -- {inputFile} {outputFile} [-f|--force] + + Arguments: + inputFile file path to read network topology from + outputFile file path to write optimized network topology to + -f or --force overwrite outputFile if it already exists + """); + return 0; +} + +var inputFilePath = args[0]; +var outputFilePath = args[1]; + +if (!File.Exists(inputFilePath)) +{ + Console.Error.WriteLine($"error: input file '{inputFilePath}' does not exist"); + return 1; +} + +var force = args.Length >= 3 && args[2] is "-f" or "--force"; + +if (!force && File.Exists(outputFilePath)) +{ + Console.Write($"File '{outputFilePath}' already exists, overwrite? (y/n): "); + if (Console.ReadLine()?.Trim() != "y") + { + Console.WriteLine("Cancelled"); + return 0; + } +} + +Graph graph; +using (var inputFile = File.OpenText(inputFilePath)) +{ + try + { + graph = Graph.ReadGraph(inputFile); + } + catch (InvalidDataException) + { + Console.Error.WriteLine($"error: invalid input file format"); + return 1; + } +} + +if (!GraphOptimizer.Optimize(graph, out var optimized)) +{ + Console.Error.WriteLine($"error: graph is disconected"); + return 1; +} + +using var outputFile = File.CreateText(outputFilePath); +optimized.Write(outputFile); + +return 0; diff --git a/Routers/Routers.Cli/Routers.Cli.csproj b/Routers/Routers.Cli/Routers.Cli.csproj new file mode 100644 index 0000000..84a6cca --- /dev/null +++ b/Routers/Routers.Cli/Routers.Cli.csproj @@ -0,0 +1,14 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + diff --git a/Routers/Routers.sln b/Routers/Routers.sln index c3ca7aa..a4f1661 100644 --- a/Routers/Routers.sln +++ b/Routers/Routers.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers", "Routers\Routers. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers.Tests", "Routers.Tests\Routers.Tests.csproj", "{91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Routers.Cli", "Routers.Cli\Routers.Cli.csproj", "{1A5C197A-399A-4808-BC0C-C033CE664E19}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -41,6 +43,18 @@ Global {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x64.Build.0 = Release|Any CPU {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.ActiveCfg = Release|Any CPU {91AF46DD-D4BB-4CAF-A8CB-E06715C013FA}.Release|x86.Build.0 = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x64.Build.0 = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Debug|x86.Build.0 = Debug|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|Any CPU.Build.0 = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x64.ActiveCfg = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x64.Build.0 = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x86.ActiveCfg = Release|Any CPU + {1A5C197A-399A-4808-BC0C-C033CE664E19}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE