From a9492e4af45bf12afb2a2591dff84f959c028d9b Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Thu, 11 Dec 2025 02:30:23 +0500 Subject: [PATCH 1/8] Made basic tag cloud generator --- .../Algorithms/BasicTagCloudAlgorithm.cs | 100 ++++++ TagCloudGenerator/Clients/ConsoleClient.cs | 332 ++++++++++++++++++ TagCloudGenerator/Clients/UiClient.cs | 17 + .../Core/Interfaces/IAnalyzer.cs | 15 + TagCloudGenerator/Core/Interfaces/IClient.cs | 13 + .../Core/Interfaces/ICloudItem.cs | 21 ++ TagCloudGenerator/Core/Interfaces/IFilter.cs | 14 + .../Core/Interfaces/IFontSizeCalculator.cs | 13 + TagCloudGenerator/Core/Interfaces/IReader.cs | 13 + .../Core/Interfaces/IRenderSettings.cs | 23 ++ .../Core/Interfaces/IRenderer.cs | 15 + .../Core/Interfaces/ITagCloudAlgorithm.cs | 14 + .../Core/Interfaces/ITagCloudGenerator.cs | 14 + .../Core/Interfaces/ITextMeasurer.cs | 14 + TagCloudGenerator/Core/Models/CloudItem.cs | 62 ++++ .../Core/Models/RenderSettings.cs | 24 ++ .../Core/Services/CloudGenerator.cs | 121 +++++++ TagCloudGenerator/DI/TagCloudModule.cs | 50 +++ .../Analyzers/WordsFrequencyAnalyzer.cs | 37 ++ .../Calculators/LinearFontSizeCalculator.cs | 21 ++ .../Filters/BoringWordsFilter.cs | 23 ++ .../Measurers/GraphicsTextMeasurer.cs | 30 ++ .../Infrastructure/Readers/LineTextReader.cs | 26 ++ .../Infrastructure/Renderers/PngRenderer.cs | 107 ++++++ TagCloudGenerator/Program.cs | 24 ++ TagCloudGenerator/TagCloudGenerator.csproj | 15 + .../BoringWordsFilterTests.cs | 94 +++++ TagCloudGeneratorTests/CloudGeneratorTests.cs | 114 ++++++ .../GraphicsTextMeasurerTests.cs | 89 +++++ TagCloudGeneratorTests/LineTextReaderTests.cs | 120 +++++++ .../LinearFontSizeCalculatorTests.cs | 82 +++++ .../TagCloudGeneratorTests.csproj | 25 ++ .../WordsFrequencyAnalyzerTests.cs | 92 +++++ di.sln | 20 +- 34 files changed, 1793 insertions(+), 1 deletion(-) create mode 100644 TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs create mode 100644 TagCloudGenerator/Clients/ConsoleClient.cs create mode 100644 TagCloudGenerator/Clients/UiClient.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IAnalyzer.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IClient.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ICloudItem.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFilter.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IReader.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IRenderSettings.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IRenderer.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs create mode 100644 TagCloudGenerator/Core/Models/CloudItem.cs create mode 100644 TagCloudGenerator/Core/Models/RenderSettings.cs create mode 100644 TagCloudGenerator/Core/Services/CloudGenerator.cs create mode 100644 TagCloudGenerator/DI/TagCloudModule.cs create mode 100644 TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs create mode 100644 TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs create mode 100644 TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs create mode 100644 TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs create mode 100644 TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs create mode 100644 TagCloudGenerator/Program.cs create mode 100644 TagCloudGenerator/TagCloudGenerator.csproj create mode 100644 TagCloudGeneratorTests/BoringWordsFilterTests.cs create mode 100644 TagCloudGeneratorTests/CloudGeneratorTests.cs create mode 100644 TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs create mode 100644 TagCloudGeneratorTests/LineTextReaderTests.cs create mode 100644 TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs create mode 100644 TagCloudGeneratorTests/TagCloudGeneratorTests.csproj create mode 100644 TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs new file mode 100644 index 00000000..df80da64 --- /dev/null +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Algorithms +{ + public class BasicTagCloudAlgorithm : ITagCloudAlgorithm + { + private Point center; + + private readonly Random random = new Random(); + + private readonly List rectangles = new List(); + + private double currentAngle = 0; + private double currentRadius = 0; + + private int countToGenerate = 10; + + private (int min, int max) width = new(20, 21); + private (int min, int max) height = new(20, 21); + + private double angleStep = 0.1; + private double radiusStep = 0.5; + + public BasicTagCloudAlgorithm(Point center) + { + if (center.X <= 0 || center.Y <= 0) throw new ArgumentException("Center coordinates must be positives"); + this.center = center; + } + + public BasicTagCloudAlgorithm() + { + this.center = new Point(0, 0); + } + + public Rectangle PutNextRectangle(Size rectangleSize) + { + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) throw new ArgumentException("Rectangle sizes must be positives"); + + if (rectangles.Count == 0) + { + return PutFirstRectangle(rectangleSize); + } + + return PlaceNextRectange(rectangleSize); + } + + private Rectangle PlaceNextRectange(Size rectangleSize) + { + while (true) + { + double x = center.X + currentRadius * Math.Cos(currentAngle); + double y = center.Y + currentRadius * Math.Sin(currentAngle); + + var potentialCenter = new Point((int)(x - rectangleSize.Width / 2), (int)(y - rectangleSize.Height / 2)); + var potentialRectangle = new Rectangle(potentialCenter, rectangleSize); + + if (!IntersectWithAny(potentialRectangle)) + { + rectangles.Add(potentialRectangle); + return potentialRectangle; + } + + currentAngle += angleStep; + if (currentAngle >= 2 * Math.PI) + { + currentAngle = 0; + currentRadius += radiusStep; + } + } + } + + private bool IntersectWithAny(Rectangle potentialRectangle) + { + return rectangles.Any(rect => rect.IntersectsWith(potentialRectangle)); + } + + private Rectangle PutFirstRectangle(Size rectangleSize) + { + var firstCenter = new Point((int)(center.X - rectangleSize.Width / 2), (int)(center.Y - rectangleSize.Width / 2)); + var first = new Rectangle(firstCenter, rectangleSize); + currentRadius = rectangleSize.Height / 2; + rectangles.Add(first); + return first; + } + + public BasicTagCloudAlgorithm WithCenterAt(Point point) + { + if (point.X <= 0 || point.Y <= 0) throw new ArgumentException("Center coordinates must be positives"); + center = point; + return this; + } + + } +} diff --git a/TagCloudGenerator/Clients/ConsoleClient.cs b/TagCloudGenerator/Clients/ConsoleClient.cs new file mode 100644 index 00000000..c0e7349b --- /dev/null +++ b/TagCloudGenerator/Clients/ConsoleClient.cs @@ -0,0 +1,332 @@ +using System; +using System.Drawing; +using System.IO; +using System.Linq; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Clients +{ + public class ConsoleClient : IClient + { + private readonly ITagCloudGenerator _generator; + + public ConsoleClient(ITagCloudGenerator generator) + { + _generator = generator; + } + + public void Run(string[] args) + { + Console.WriteLine("=== Tag Cloud Generator ==="); + Console.WriteLine("Type '--help' to see available commands"); + Console.WriteLine(); + + while (true) + { + Console.Write("> "); + var commandLine = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(commandLine)) + continue; + + if (commandLine == "exit" || commandLine == "quit") + break; + + var commandArgs = ParseCommandLine(commandLine); + + if (commandArgs.Length == 0) + continue; + + if (commandArgs[0] == "--help" || commandArgs[0] == "-h") + { + PrintHelp(); + continue; + } + + var result = ParseArguments(commandArgs); + if (!result.IsValid) + { + Console.WriteLine("Error: Input file is required!"); + Console.WriteLine("Usage: [options]"); + Console.WriteLine("Use --help for more information"); + continue; + } + + if (!File.Exists(result.InputFile)) + { + Console.WriteLine($"Error: File '{result.InputFile}' not found!"); + continue; + } + + Console.WriteLine("\nSettings:"); + PrintSettings(result); + + Console.WriteLine("\nStarting generation..."); + try + { + _generator.Generate(result.InputFile, result.OutputFile, result.RenderSettings); + Console.WriteLine($"\nSuccess! Image saved to bin folder of the project as: {result.OutputFile}"); + } + catch (Exception ex) + { + Console.WriteLine($"\nError: {ex.Message}"); + } + + Console.WriteLine(); + } + } + + private string[] ParseCommandLine(string commandLine) + { + var parts = new System.Collections.Generic.List(); + var currentPart = new System.Text.StringBuilder(); + bool inQuotes = false; + + for (int i = 0; i < commandLine.Length; i++) + { + char c = commandLine[i]; + + if (c == '"') + { + inQuotes = !inQuotes; + } + else if (c == ' ' && !inQuotes) + { + if (currentPart.Length > 0) + { + parts.Add(currentPart.ToString()); + currentPart.Clear(); + } + } + else + { + currentPart.Append(c); + } + } + + if (currentPart.Length > 0) + { + parts.Add(currentPart.ToString()); + } + + return parts.ToArray(); + } + + private ParseResult ParseArguments(string[] args) + { + var result = new ParseResult + { + RenderSettings = new RenderSettings + { + FontFamily = "Arial", + MinFontSize = 12f, + MaxFontSize = 72f, + BackgroundColor = Color.White, + TextColor = Color.Black, + CanvasSize = new Size(800, 600), + CenterCloud = true, + ShowRectangles = false, + Padding = 50 + } + }; + + string inputFile = null; + for (int i = 0; i < args.Length; i++) + { + if (!args[i].StartsWith("-") && !args[i].StartsWith("--")) + { + if (inputFile == null && args[i].EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) + { + inputFile = args[i]; + } + } + } + + if (inputFile == null) + { + result.IsValid = false; + return result; + } + + result.InputFile = inputFile; + result.OutputFile = GenerateOutputFilename(inputFile); + result.IsValid = true; + + for (int i = 0; i < args.Length; i++) + { + switch (args[i].ToLower()) + { + case "--font": + case "-f": + if (i + 1 < args.Length && !IsFlag(args[i + 1])) + { + result.RenderSettings.FontFamily = args[++i]; + } + break; + + case "--background": + case "-bg": + if (i + 1 < args.Length && !IsFlag(args[i + 1])) + { + result.RenderSettings.BackgroundColor = ParseColor(args[++i], result.RenderSettings.BackgroundColor); + } + break; + + case "--text-color": + case "-tc": + if (i + 1 < args.Length && !IsFlag(args[i + 1])) + { + result.RenderSettings.TextColor = ParseColor(args[++i], result.RenderSettings.TextColor); + } + break; + + case "--width": + case "-w": + if (i + 1 < args.Length && int.TryParse(args[i + 1], out int width)) + { + result.RenderSettings.CanvasSize = new Size(width, result.RenderSettings.CanvasSize.Height); + i++; + } + break; + + case "--height": + if (i + 1 < args.Length && int.TryParse(args[i + 1], out int height)) + { + result.RenderSettings.CanvasSize = new Size(result.RenderSettings.CanvasSize.Width, height); + i++; + } + break; + + case "--show-rectangles": + result.RenderSettings.ShowRectangles = true; + break; + + case "--output": + case "-o": + if (i + 1 < args.Length && !IsFlag(args[i + 1])) + { + result.OutputFile = args[++i]; + if (!result.OutputFile.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) + result.OutputFile += ".png"; + } + break; + } + } + + return result; + } + + private bool IsFlag(string arg) + { + return arg.StartsWith("-") || arg.StartsWith("--"); + } + + private string GenerateOutputFilename(string inputFile) + { + string fileName = Path.GetFileNameWithoutExtension(inputFile); + string directory = Path.Combine(Path.GetDirectoryName(inputFile), "CLouds"); + Directory.CreateDirectory(directory); + + string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); + string outputFile = $"{fileName}_cloud_{timestamp}.png"; + + if (!string.IsNullOrEmpty(directory)) + { + outputFile = Path.Combine(directory, outputFile); + } + + return outputFile; + } + + private Color ParseColor(string colorString, Color defaultColor) + { + try + { + var color = Color.FromName(colorString); + if (color.IsKnownColor) + return color; + + if (colorString.StartsWith("#")) + { + return ColorTranslator.FromHtml(colorString); + } + + if (colorString.Contains(",")) + { + var parts = colorString.Split(','); + if (parts.Length == 3) + { + return Color.FromArgb( + int.Parse(parts[0].Trim()), + int.Parse(parts[1].Trim()), + int.Parse(parts[2].Trim())); + } + } + + return defaultColor; + } + catch + { + return defaultColor; + } + } + + private void PrintHelp() + { + Console.WriteLine("=== Tag Cloud Generator Help ==="); + Console.WriteLine(); + Console.WriteLine("Usage:"); + Console.WriteLine(" [options]"); + Console.WriteLine(" --help"); + Console.WriteLine(); + Console.WriteLine("Arguments:"); + Console.WriteLine(" Input text file (required)"); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" -f, --font Font family (default: Arial)"); + Console.WriteLine(" -bg, --background Background color (default: White)"); + Console.WriteLine(" -tc, --text-color Text color (default: Black)"); + Console.WriteLine(" -w, --width Canvas width (default: 800)"); + Console.WriteLine(" --height Canvas height (default: 600)"); + Console.WriteLine(" -o, --output Output file (default: _cloud_.png)"); + Console.WriteLine(" --show-rectangles Show word rectangles (debug mode)"); + Console.WriteLine(" --help Show this help message"); + Console.WriteLine(); + Console.WriteLine("Color formats:"); + Console.WriteLine(" - Named colors: White, Black, Red, Blue, Green, etc."); + Console.WriteLine(" - HEX: #FFFFFF, #000000, #FF0000"); + Console.WriteLine(" - RGB: 255,255,255 or 0,0,0"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" document.txt"); + Console.WriteLine(" words.txt --font \"Times New Roman\""); + Console.WriteLine(" input.txt -bg White -tc Navy --width 1200 --height 800"); + Console.WriteLine(" text.txt --background #F0F0F0 --text-color #333333 -o cloud.png"); + Console.WriteLine(" data.txt --show-rectangles"); + Console.WriteLine(); + Console.WriteLine("Commands:"); + Console.WriteLine(" --help Show this help"); + Console.WriteLine(" exit, quit Exit program"); + } + + private void PrintSettings(ParseResult result) + { + Console.WriteLine($" Input file: {result.InputFile}"); + Console.WriteLine($" Output file: {result.OutputFile}"); + Console.WriteLine($" Font: {result.RenderSettings.FontFamily}"); + Console.WriteLine($" Background: {result.RenderSettings.BackgroundColor.Name}"); + Console.WriteLine($" Text color: {result.RenderSettings.TextColor.Name}"); + Console.WriteLine($" Canvas size: {result.RenderSettings.CanvasSize.Width}x{result.RenderSettings.CanvasSize.Height}"); + Console.WriteLine($" Show rectangles: {result.RenderSettings.ShowRectangles}"); + } + + private class ParseResult + { + public bool IsValid { get; set; } + public string InputFile { get; set; } + public string OutputFile { get; set; } + public RenderSettings RenderSettings { get; set; } + } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Clients/UiClient.cs b/TagCloudGenerator/Clients/UiClient.cs new file mode 100644 index 00000000..28dce0e5 --- /dev/null +++ b/TagCloudGenerator/Clients/UiClient.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Clients +{ + public class UiClient : IClient + { + public void Run(string[] args) + { + throw new NotImplementedException(); + } + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs new file mode 100644 index 00000000..232ab84f --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IAnalyzer + { + IEnumerable Analyze(IEnumerable words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs new file mode 100644 index 00000000..32b64b0c --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IClient + { + void Run(string[] args); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ICloudItem.cs b/TagCloudGenerator/Core/Interfaces/ICloudItem.cs new file mode 100644 index 00000000..7d8b387c --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ICloudItem.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ICloudItem + { + string Word { get; } + Rectangle Rectangle { get; } + float FontSize { get; } + Color? Color { get; } + string FontFamily { get; } + FontStyle FontStyle { get; } + int Frequency { get; } + double Weight { get; } + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFilter.cs b/TagCloudGenerator/Core/Interfaces/IFilter.cs new file mode 100644 index 00000000..ed65c180 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFilter.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFilter + { + IEnumerable Filter(IEnumerable words); + bool ShouldInclude(string word); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs new file mode 100644 index 00000000..a61fabab --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFontSizeCalculator + { + float Calculate(int wordFrequency, int minFrequency, int maxFrequency, float minFontSize, float maxFontSize); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IReader.cs b/TagCloudGenerator/Core/Interfaces/IReader.cs new file mode 100644 index 00000000..a01af628 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IReader.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IReader + { + IEnumerable TryRead(string filePath); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs b/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs new file mode 100644 index 00000000..9970b529 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IRenderSettings + { + string OutputPath { get; set; } + string FontFamily { get; set; } + float MinFontSize { get; set; } + float MaxFontSize { get; set; } + Color BackgroundColor { get; set; } + Color TextColor { get; set; } + Size CanvasSize { get; set; } + bool CenterCloud { get; set; } + bool ShowRectangles { get; set; } + int Padding { get; set; } + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs new file mode 100644 index 00000000..e9f9c4cb --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IRenderer + { + void Render(IEnumerable items, RenderSettings settings); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs new file mode 100644 index 00000000..833627b0 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITagCloudAlgorithm + { + Rectangle PutNextRectangle(Size rectangleSize); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs new file mode 100644 index 00000000..e1a8b970 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITagCloudGenerator + { + public void Generate(string inputFile, string outputFile, RenderSettings settings); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs new file mode 100644 index 00000000..f29aca0e --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITextMeasurer + { + Size Measure(string word, float fontSize, string fontFamily); + } +} diff --git a/TagCloudGenerator/Core/Models/CloudItem.cs b/TagCloudGenerator/Core/Models/CloudItem.cs new file mode 100644 index 00000000..7f3674ce --- /dev/null +++ b/TagCloudGenerator/Core/Models/CloudItem.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Core.Models +{ + public class CloudItem : ICloudItem + { + public string Word { get; } + public Rectangle Rectangle { get; } + public float FontSize { get; } + public Color? Color { get; } + public string FontFamily { get; } + public FontStyle FontStyle { get; } + public int Frequency { get; } + public double Weight { get; } + + public CloudItem( + string word, + Rectangle rectangle, + float fontSize, + Color? color = null, + string fontFamily = "Arial", + FontStyle fontStyle = FontStyle.Regular, + int frequency = 1, + double weight = 1.0) + { + Word = word ?? throw new ArgumentNullException(nameof(word)); + Rectangle = rectangle; + FontSize = fontSize; + Color = color; + FontFamily = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily)); + FontStyle = fontStyle; + Frequency = frequency; + Weight = weight; + } + + public static CloudItem Create(string word, Rectangle rectangle, float fontSize) + { + return new CloudItem(word, rectangle, fontSize); + } + + public CloudItem WithRectangle(Rectangle newRectangle) + { + return new CloudItem(Word, newRectangle, FontSize, Color, FontFamily, FontStyle, Frequency, Weight); + } + + public CloudItem WithFontSize(float newFontSize) + { + return new CloudItem(Word, Rectangle, newFontSize, Color, FontFamily, FontStyle, Frequency, Weight); + } + + public CloudItem WithColor(Color newColor) + { + return new CloudItem(Word, Rectangle, FontSize, newColor, FontFamily, FontStyle, Frequency, Weight); + } + } +} diff --git a/TagCloudGenerator/Core/Models/RenderSettings.cs b/TagCloudGenerator/Core/Models/RenderSettings.cs new file mode 100644 index 00000000..b7ab9e6b --- /dev/null +++ b/TagCloudGenerator/Core/Models/RenderSettings.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Core.Models +{ + public class RenderSettings : IRenderSettings + { + public string OutputPath { get; set; } + public string FontFamily { get; set; } = "Arial"; + public float MinFontSize { get; set; } = 12f; + public float MaxFontSize { get; set; } = 72f; + public Color BackgroundColor { get; set; } + public Color TextColor { get; set; } + public Size CanvasSize { get; set; } + public bool CenterCloud { get; set; } + public bool ShowRectangles { get; set; } + public int Padding { get; set; } = 50; + } +} diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs new file mode 100644 index 00000000..6f53bcf9 --- /dev/null +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Services +{ + public class CloudGenerator : ITagCloudGenerator + { + private readonly ITagCloudAlgorithm _algorithm; + private readonly IReader _reader; + private readonly IEnumerable _filters; + private readonly IAnalyzer _analyzer; + private readonly IRenderer _renderer; + private readonly IFontSizeCalculator _fontSizeCalculator; + private readonly ITextMeasurer _textMeasurer; + private readonly Point _center; + + public CloudGenerator(ITagCloudAlgorithm algorithm, + IReader reader, + IEnumerable filters, + IAnalyzer analyzer, + IRenderer renderer, + IFontSizeCalculator fontSizeCalculator, + ITextMeasurer textMeasurer) + { + _algorithm = algorithm; + _reader = reader; + _filters = filters; + _analyzer = analyzer; + _renderer = renderer; + _fontSizeCalculator = fontSizeCalculator; + _textMeasurer = textMeasurer; + _center = new Point(0, 0); + } + + public void Generate(string inputFile, string outputFile, RenderSettings renderSettings) + { + var words = _reader.TryRead(inputFile); + if (words == null || !words.Any()) + { + return; + } + + words = ApplyFilters(words); + if (!words.Any()) + { + return; + } + + var cloudItems = _analyzer.Analyze(words).ToList(); + + var preparedItems = PrepareCloudItems(cloudItems, renderSettings).ToList(); + + var arrangedItems = ArrangeCloudItems(preparedItems).ToList(); + + renderSettings.OutputPath = outputFile; + + _renderer.Render(arrangedItems, renderSettings); + } + + private IEnumerable ApplyFilters(IEnumerable words) + { + var filteredWords = words; + foreach (var filter in _filters) + { + filteredWords = filter.Filter(filteredWords); + } + return filteredWords; + } + + private IEnumerable PrepareCloudItems(IEnumerable items, RenderSettings settings) + { + var itemsList = items.ToList(); + var frequencies = itemsList.Select(i => i.Frequency).ToList(); + var minFrequency = frequencies.Min(); + var maxFrequency = frequencies.Max(); + + for (int i = 0; i < itemsList.Count; i++) + { + var item = itemsList[i]; + + var fontSize = _fontSizeCalculator.Calculate( + item.Frequency, + minFrequency, + maxFrequency, + settings.MinFontSize, + settings.MaxFontSize); + + var textSize = _textMeasurer.Measure( + item.Word, + fontSize, + settings.FontFamily); + + yield return new CloudItem( + word: item.Word, + rectangle: new Rectangle(Point.Empty, textSize), + fontSize: fontSize, + color: settings.TextColor, + fontFamily: settings.FontFamily, + frequency: item.Frequency, + weight: item.Weight + ); + } + } + + private IEnumerable ArrangeCloudItems(IEnumerable items) + { + + foreach (var item in items) + { + var newRectangle = _algorithm.PutNextRectangle(item.Rectangle.Size); + yield return item.WithRectangle(newRectangle); + } + } + } +} diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs new file mode 100644 index 00000000..40277cd1 --- /dev/null +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -0,0 +1,50 @@ +using Autofac; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Algorithms; +using TagCloudGenerator.Clients; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Services; +using TagCloudGenerator.Infrastructure.Analyzers; +using TagCloudGenerator.Infrastructure.Calculators; +using TagCloudGenerator.Infrastructure.Filters; +using TagCloudGenerator.Infrastructure.Measurers; +using TagCloudGenerator.Infrastructure.Reader; +using TagCloudGenerator.Infrastructure.Renderers; + +namespace TagCloudGenerator.DI +{ + public class TagCloudModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.RegisterType() + .As() + .WithParameter("minFontSize", 12f) + .WithParameter("maxFontSize", 72f); + + builder.RegisterType() + .As(); + + builder.RegisterType() + .As(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs new file mode 100644 index 00000000..cc099b30 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Analyzers +{ + public class WordsFrequencyAnalyzer : IAnalyzer + { + public IEnumerable Analyze(IEnumerable words) + { + var wordGroups = words + .GroupBy(word => word) + .Select(group => new + { + Word = group.Key, + Frequency = group.Count() + }) + .OrderByDescending(x => x.Frequency); + + foreach (var group in wordGroups) + { + yield return new CloudItem( + word: group.Word, + rectangle: Rectangle.Empty, + fontSize: 0, + frequency: group.Frequency, + weight: (double)group.Frequency / wordGroups.Max(g => g.Frequency) + ); + } + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs new file mode 100644 index 00000000..8b90b15b --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Calculators +{ + public class LinearFontSizeCalculator : IFontSizeCalculator + { + public float Calculate(int wordFrequency, int minFrequency, int maxFrequency, float minFontSize, float maxFontSize) + { + if (minFrequency == maxFrequency) + return (minFontSize + maxFontSize) / 2; + + float frequencyRatio = (float)(wordFrequency - minFrequency) / (maxFrequency - minFrequency); + return minFontSize + frequencyRatio * (maxFontSize - minFontSize); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs new file mode 100644 index 00000000..7b45c49f --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Filters +{ + public class BoringWordsFilter : IFilter + { + private readonly string[] _boringWords = ["in", "it", "a", "as", "for", "of", "on"]; + public IEnumerable Filter(IEnumerable words) + { + return words.Where(w => ShouldInclude(w)); + } + + public bool ShouldInclude(string word) + { + return !_boringWords.Contains(word); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs new file mode 100644 index 00000000..390be0fa --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Measurers +{ + public class GraphicsTextMeasurer : ITextMeasurer + { + private readonly string _fontFamily; + + public GraphicsTextMeasurer(string fontFamily = "Arial") + { + _fontFamily = fontFamily; + } + + public Size Measure(string word, float fontSize, string fontFamily) + { + using var font = new Font(fontFamily, fontSize); + using var bitmap = new Bitmap(1, 1); + using var graphics = Graphics.FromImage(bitmap); + + var size = graphics.MeasureString(word, font); + return new Size((int)Math.Ceiling(size.Width), (int)Math.Ceiling(size.Height)); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs b/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs new file mode 100644 index 00000000..000f6d96 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Reader +{ + public class LineTextReader : IReader + { + public IEnumerable? TryRead(string filePath) + { + try + { + string[] fileContent = File.ReadAllLines(filePath); + return fileContent.Select(w => w.ToLower()); + } + catch(IOException e) + { + Console.WriteLine($"Error reading file: {e.Message}"); + return null; + } + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs new file mode 100644 index 00000000..619dc832 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -0,0 +1,107 @@ +using System; +using System.Collections.Generic; +using System.Drawing.Imaging; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using System.IO; + +namespace TagCloudGenerator.Infrastructure.Renderers +{ + public class PngRenderer : IRenderer + { + public void Render(IEnumerable items, RenderSettings settings) + { + var itemsList = items.ToList(); + if (!itemsList.Any()) + { + Console.WriteLine("No elements to render"); + return; + } + + using var bitmap = new Bitmap(settings.CanvasSize.Width, settings.CanvasSize.Height); + using var graphics = Graphics.FromImage(bitmap); + + ConfigureGraphics(graphics); + graphics.Clear(settings.BackgroundColor); + + var (offsetX, offsetY) = CalculateOffset(itemsList, settings); + + foreach (var item in itemsList) + { + DrawCloudItem(graphics, item, offsetX, offsetY, settings); + } + + bitmap.Save(settings.OutputPath, ImageFormat.Png); + } + + private (int offsetX, int offsetY) CalculateOffset( + List items, + RenderSettings settings) + { + if (!settings.CenterCloud) + return (settings.Padding, settings.Padding); + + var minX = items.Min(i => i.Rectangle.X); + var minY = items.Min(i => i.Rectangle.Y); + var maxX = items.Max(i => i.Rectangle.Right); + var maxY = items.Max(i => i.Rectangle.Bottom); + + var cloudWidth = maxX - minX; + var cloudHeight = maxY - minY; + + var offsetX = (settings.CanvasSize.Width - cloudWidth) / 2 - minX; + var offsetY = (settings.CanvasSize.Height - cloudHeight) / 2 - minY; + + return (offsetX, offsetY); + } + + private void DrawCloudItem( + Graphics graphics, + CloudItem item, + int offsetX, + int offsetY, + RenderSettings settings) + { + var drawRect = new Rectangle( + item.Rectangle.X + offsetX, + item.Rectangle.Y + offsetY, + item.Rectangle.Width, + item.Rectangle.Height); + + var color = item.Color ?? settings.TextColor; + + using var font = new Font( + item.FontFamily, + item.FontSize, + item.FontStyle, + GraphicsUnit.Pixel); + + using var brush = new SolidBrush(color); + using var stringFormat = new StringFormat + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center + }; + + graphics.DrawString(item.Word, font, brush, drawRect, stringFormat); + + if (settings.ShowRectangles) + { + using var pen = new Pen(color, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; + graphics.DrawRectangle(pen, drawRect); + } + } + + private void ConfigureGraphics(Graphics graphics) + { + graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias; + graphics.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; + graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; + graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; + } + } +} diff --git a/TagCloudGenerator/Program.cs b/TagCloudGenerator/Program.cs new file mode 100644 index 00000000..146a99de --- /dev/null +++ b/TagCloudGenerator/Program.cs @@ -0,0 +1,24 @@ +using System; +using Autofac; +using TagCloudGenerator.Clients; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.DI; + +namespace TagCloudGenerator +{ + public class Program + { + public static void Main(string[] args) + { + var builder = new ContainerBuilder(); + + builder.RegisterModule(); + + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var client = scope.Resolve(); + client.Run(args); + } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj new file mode 100644 index 00000000..15081f91 --- /dev/null +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -0,0 +1,15 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + diff --git a/TagCloudGeneratorTests/BoringWordsFilterTests.cs b/TagCloudGeneratorTests/BoringWordsFilterTests.cs new file mode 100644 index 00000000..29c1f109 --- /dev/null +++ b/TagCloudGeneratorTests/BoringWordsFilterTests.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using System.Linq; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudGeneratorTests +{ + public class BoringWordsFilterTests + { + private BoringWordsFilter filter; + + [SetUp] + public void Setup() + { + filter = new BoringWordsFilter(); + } + + [Test] + public void Filter_EmptyInput_ReturnsEmpty() + { + var words = Enumerable.Empty(); + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_RemovesBoringWords() + { + var words = new[] { "hello", "in", "world", "a", "test" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result, Contains.Item("hello")); + Assert.That(result, Contains.Item("world")); + Assert.That(result, Contains.Item("test")); + Assert.That(result, Does.Not.Contain("in")); + Assert.That(result, Does.Not.Contain("a")); + } + + [Test] + public void Filter_AllBoringWords_ReturnsEmpty() + { + var words = new[] { "in", "a", "for", "on" }; + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_NoBoringWords_ReturnsAllWords() + { + var words = new[] { "hello", "world", "test" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result, Is.EquivalentTo(words)); + } + + [Test] + public void ShouldInclude_ReturnsFalseForBoringWords() + { + var boringWords = new[] { "in", "it", "a", "as", "for", "of", "on" }; + foreach (var word in boringWords) + { + Assert.That(filter.ShouldInclude(word), Is.False); + } + } + + [Test] + public void ShouldInclude_ReturnsTrueForNormalWords() + { + var normalWords = new[] { "hello", "world", "computer", "programming", "test" }; + foreach (var word in normalWords) + { + Assert.That(filter.ShouldInclude(word), Is.True); + } + } + + [Test] + public void Filter_PreservesOrder() + { + var words = new[] { "hello", "in", "world", "a", "test", "for" }; + var result = filter.Filter(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0], Is.EqualTo("hello")); + Assert.That(result[1], Is.EqualTo("world")); + Assert.That(result[2], Is.EqualTo("test")); + } + } +} diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs new file mode 100644 index 00000000..402f87b3 --- /dev/null +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Moq; +using NUnit.Framework; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Services; + + +namespace TagCloudGeneratorTests +{ + public class CloudGeneratorTests + { + private Mock algorithmMock; + private Mock readerMock; + private Mock filterMock; + private Mock analyzerMock; + private Mock rendererMock; + private Mock fontSizeCalculatorMock; + private Mock textMeasurerMock; + private CloudGenerator cloudGenerator; + + [SetUp] + public void Setup() + { + algorithmMock = new Mock(); + readerMock = new Mock(); + filterMock = new Mock(); + analyzerMock = new Mock(); + rendererMock = new Mock(); + fontSizeCalculatorMock = new Mock(); + textMeasurerMock = new Mock(); + + cloudGenerator = new CloudGenerator( + algorithmMock.Object, + readerMock.Object, + new[] { filterMock.Object }, + analyzerMock.Object, + rendererMock.Object, + fontSizeCalculatorMock.Object, + textMeasurerMock.Object); + } + + [Test] + public void Generate_EmptyFile_DoesNothing() + { + var words = Enumerable.Empty(); + readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(words); + + cloudGenerator.Generate("input.txt", "output.png", new RenderSettings()); + + rendererMock.Verify(r => r.Render(It.IsAny>(), It.IsAny()), Times.Never); + } + + [Test] + public void Generate_AllWordsFilteredOut_DoesNothing() + { + var words = new[] { "in", "a", "for" }; + readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(words); + filterMock.Setup(f => f.Filter(It.IsAny>())).Returns(Enumerable.Empty()); + + cloudGenerator.Generate("input.txt", "output.png", new RenderSettings()); + + rendererMock.Verify(r => r.Render(It.IsAny>(), It.IsAny()), Times.Never); + } + + [Test] + public void Generate_NormalFlow_CallsAllDependencies() + { + var words = new[] { "hello", "world", "test" }; + var filteredWords = new[] { "hello", "world", "test" }; + var cloudItems = new[] + { + new CloudItem("hello", Rectangle.Empty, 0, frequency: 2), + new CloudItem("world", Rectangle.Empty, 0, frequency: 1) + }; + var renderSettings = new RenderSettings + { + MinFontSize = 12f, + MaxFontSize = 72f, + FontFamily = "Arial" + }; + + readerMock.Setup(r => r.TryRead("input.txt")).Returns(words); + filterMock.Setup(f => f.Filter(words)).Returns(filteredWords); + analyzerMock.Setup(a => a.Analyze(filteredWords)).Returns(cloudItems); + + fontSizeCalculatorMock.Setup(f => f.Calculate(2, 1, 2, 12f, 72f)).Returns(50f); + fontSizeCalculatorMock.Setup(f => f.Calculate(1, 1, 2, 12f, 72f)).Returns(20f); + + textMeasurerMock.Setup(t => t.Measure("hello", 50f, "Arial")).Returns(new Size(100, 30)); + textMeasurerMock.Setup(t => t.Measure("world", 20f, "Arial")).Returns(new Size(80, 25)); + + algorithmMock.Setup(a => a.PutNextRectangle(new Size(100, 30))).Returns(new Rectangle(0, 0, 100, 30)); + algorithmMock.Setup(a => a.PutNextRectangle(new Size(80, 25))).Returns(new Rectangle(100, 0, 80, 25)); + + cloudGenerator.Generate("input.txt", "output.png", renderSettings); + + readerMock.Verify(r => r.TryRead("input.txt"), Times.Once); + filterMock.Verify(f => f.Filter(words), Times.Once); + analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); + fontSizeCalculatorMock.Verify(f => f.Calculate(It.IsAny(), 1, 2, 12f, 72f), Times.Exactly(2)); + textMeasurerMock.Verify(t => t.Measure(It.IsAny(), It.IsAny(), "Arial"), Times.Exactly(2)); + algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); + rendererMock.Verify(r => r.Render(It.IsAny>(), It.Is(s => s.OutputPath == "output.png")), Times.Once); + } + } +} diff --git a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs new file mode 100644 index 00000000..42ebd96d --- /dev/null +++ b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using System.Drawing; +using TagCloudGenerator.Infrastructure.Measurers; + +namespace TagCloudGeneratorTests +{ + public class GraphicsTextMeasurerTests + { + private GraphicsTextMeasurer measurer; + + [SetUp] + public void Setup() + { + measurer = new GraphicsTextMeasurer("Arial"); + } + + [Test] + public void Measure_EmptyString_ReturnsValidSize() + { + var result = measurer.Measure("", 12f, "Arial"); + Assert.That(result.Width, Is.GreaterThanOrEqualTo(0)); + Assert.That(result.Height, Is.GreaterThanOrEqualTo(0)); + } + + [Test] + public void Measure_SingleCharacter_ReturnsNonZeroSize() + { + var result = measurer.Measure("A", 12f, "Arial"); + Assert.That(result.Width, Is.GreaterThan(0)); + Assert.That(result.Height, Is.GreaterThan(0)); + } + + [Test] + public void Measure_LongerWord_ReturnsWiderSize() + { + var shortSize = measurer.Measure("A", 12f, "Arial"); + var longSize = measurer.Measure("ABCDEFGHIJ", 12f, "Arial"); + Assert.That(longSize.Width, Is.GreaterThan(shortSize.Width)); + } + + [Test] + public void Measure_LargerFont_ReturnsLargerSize() + { + var smallSize = measurer.Measure("Test", 12f, "Arial"); + var largeSize = measurer.Measure("Test", 24f, "Arial"); + Assert.That(largeSize.Width, Is.GreaterThan(smallSize.Width)); + Assert.That(largeSize.Height, Is.GreaterThan(smallSize.Height)); + } + + [Test] + public void Measure_DifferentFontFamily_ReturnsDifferentSize() + { + var arialSize = measurer.Measure("Test", 12f, "Arial"); + var timesSize = measurer.Measure("Test", 12f, "Times New Roman"); + Assert.That(timesSize.Width, Is.GreaterThan(0)); + Assert.That(timesSize.Height, Is.GreaterThan(0)); + } + + [Test] + public void Measure_SameParametersTwice_ReturnsSameSize() + { + var size1 = measurer.Measure("Consistent", 16f, "Arial"); + var size2 = measurer.Measure("Consistent", 16f, "Arial"); + Assert.That(size1.Width, Is.EqualTo(size2.Width)); + Assert.That(size1.Height, Is.EqualTo(size2.Height)); + } + + [Test] + public void Measure_WithSpaces_ReturnsCorrectSize() + { + var sizeWithoutSpace = measurer.Measure("HelloWorld", 12f, "Arial"); + var sizeWithSpace = measurer.Measure("Hello World", 12f, "Arial"); + Assert.That(sizeWithSpace.Width, Is.GreaterThan(sizeWithoutSpace.Width)); + } + + [Test] + public void Measure_VeryLargeFont_ReturnsProportionalSize() + { + var result = measurer.Measure("Test", 100f, "Arial"); + Assert.That(result.Width, Is.GreaterThan(50)); + Assert.That(result.Height, Is.GreaterThan(50)); + } + } +} diff --git a/TagCloudGeneratorTests/LineTextReaderTests.cs b/TagCloudGeneratorTests/LineTextReaderTests.cs new file mode 100644 index 00000000..cad4d900 --- /dev/null +++ b/TagCloudGeneratorTests/LineTextReaderTests.cs @@ -0,0 +1,120 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +using NUnit.Framework; +using System.IO; +using System.Linq; +using TagCloudGenerator.Infrastructure.Reader; + +namespace TagCloudGeneratorTests +{ + public class LineTextReaderTests + { + private LineTextReader reader; + private string tempFilePath; + + [SetUp] + public void Setup() + { + reader = new LineTextReader(); + tempFilePath = Path.GetTempFileName(); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + + private void WriteToTempFile(string content) + { + File.WriteAllText(tempFilePath, content); + } + + [Test] + public void TryRead_ExistingFile_ReturnsLines() + { + var expectedLines = new[] { "line1", "line2", "line3" }; + WriteToTempFile(string.Join(Environment.NewLine, expectedLines)); + + var result = reader.TryRead(tempFilePath).ToList(); + Assert.That(result, Is.EqualTo(expectedLines)); + } + + [Test] + public void TryRead_EmptyFile_ReturnsEmpty() + { + WriteToTempFile(""); + var result = reader.TryRead(tempFilePath); + Assert.That(result, Is.Empty); + } + + [Test] + public void TryRead_FileWithWhitespaceLines_ReturnsAllLines() + { + WriteToTempFile("line1\n\nline2\n \nline3"); + var result = reader.TryRead(tempFilePath).ToList(); + + Assert.That(result.Count, Is.EqualTo(5)); + Assert.That(result[0], Is.EqualTo("line1")); + Assert.That(result[1], Is.EqualTo("")); + Assert.That(result[2], Is.EqualTo("line2")); + Assert.That(result[3], Is.EqualTo(" ")); + Assert.That(result[4], Is.EqualTo("line3")); + } + + [Test] + public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines() + { + WriteToTempFile("line1\r\nline2\r\nline3"); + var result = reader.TryRead(tempFilePath).ToList(); + + Assert.That(result, Is.EqualTo(new[] { "line1", "line2", "line3" })); + } + + [Test] + public void TryRead_NonExistentFile_ReturnsNull() + { + var nonExistentPath = "C:\\nonexistent\\file.txt"; + var result = reader.TryRead(nonExistentPath); + Assert.That(result, Is.Null); + } + + [Test] + public void TryRead_FileWithOneLine_ReturnsOneLine() + { + WriteToTempFile("single line"); + var result = reader.TryRead(tempFilePath).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0], Is.EqualTo("single line")); + } + + [Test] + public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines() + { + WriteToTempFile("line1\nline2\n"); + var result = reader.TryRead(tempFilePath).ToList(); + + Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result, Is.EqualTo(new[] { "line1", "line2" })); + } + + [Test] + public void TryRead_LargeFile_ReturnsAllLines() + { + var lines = Enumerable.Range(1, 1000).Select(i => $"Line {i}"); + WriteToTempFile(string.Join(Environment.NewLine, lines)); + + var result = reader.TryRead(tempFilePath).ToList(); + + Assert.That(result.Count, Is.EqualTo(1000)); + Assert.That(result[0], Is.EqualTo("line 1")); + Assert.That(result[999], Is.EqualTo("line 1000")); + } + } +} diff --git a/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs new file mode 100644 index 00000000..b35f4353 --- /dev/null +++ b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs @@ -0,0 +1,82 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Calculators; + +namespace TagCloudGeneratorTests +{ + public class LinearFontSizeCalculatorTests + { + private LinearFontSizeCalculator calculator; + + [SetUp] + public void Setup() + { + calculator = new LinearFontSizeCalculator(); + } + + [Test] + public void Calculate_MinFrequency_ReturnsMinFontSize() + { + var result = calculator.Calculate(1, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(12f)); + } + + [Test] + public void Calculate_MaxFrequency_ReturnsMaxFontSize() + { + var result = calculator.Calculate(10, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(72f)); + } + + [Test] + public void Calculate_MiddleFrequency_ReturnsProportionalSize() + { + var result = calculator.Calculate(5, 1, 9, 10f, 50f); + Assert.That(result, Is.EqualTo(30f).Within(0.001f)); + } + + [Test] + public void Calculate_SameMinMaxFrequency_ReturnsAverage() + { + var result = calculator.Calculate(5, 5, 5, 12f, 72f); + Assert.That(result, Is.EqualTo(42f)); + } + + [Test] + public void Calculate_FrequencyBelowMin_ReturnsLessThanMinFontSize() + { + var result = calculator.Calculate(0, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(5.333f).Within(0.001f)); + } + + [Test] + public void Calculate_FrequencyAboveMax_ReturnsMoreThanMaxFontSize() + { + var result = calculator.Calculate(15, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(105.333f).Within(0.001f)); + } + + [Test] + public void Calculate_ZeroRangeFontSizes_ReturnsMinFontSize() + { + var result = calculator.Calculate(5, 1, 10, 20f, 20f); + Assert.That(result, Is.EqualTo(20f)); + } + + [Test] + [TestCase(1, 10f)] + [TestCase(3, 20f)] + [TestCase(5, 30f)] + [TestCase(7, 40f)] + [TestCase(9, 50f)] + public void Calculate_MultipleTestCases_ReturnsCorrectValues(int frequency, float expected) + { + var result = calculator.Calculate(frequency, 1, 9, 10f, 50f); + Assert.That(result, Is.EqualTo(expected).Within(0.001f)); + } + } +} diff --git a/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj new file mode 100644 index 00000000..3480a5e0 --- /dev/null +++ b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs new file mode 100644 index 00000000..fd019777 --- /dev/null +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -0,0 +1,92 @@ +using TagCloudGenerator.Infrastructure.Analyzers; +using TagCloudGenerator.Infrastructure.Calculators; +using NUnit.Framework; +using System.Linq; + +namespace TagCloudGeneratorTests +{ + public class WordsFrequencyAnalyzerTests + { + private WordsFrequencyAnalyzer frequencyAnalyzer; + + [SetUp] + public void Setup() + { + frequencyAnalyzer = new WordsFrequencyAnalyzer(); + } + + [Test] + public void Analyze_EmptyInput_ReturnsEmpty() + { + var words = Enumerable.Empty(); + var result = frequencyAnalyzer.Analyze(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne() + { + var words = new[] { "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].Word, Is.EqualTo("hello")); + Assert.That(result[0].Frequency, Is.EqualTo(1)); + } + + [Test] + public void Analyze_MultipleWords_ReturnsCorrectFrequencies() + { + var words = new[] { "hello", "world", "hello", "test", "world", "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result[0].Word, Is.EqualTo("hello")); + Assert.That(result[0].Frequency, Is.EqualTo(3)); + Assert.That(result[1].Word, Is.EqualTo("world")); + Assert.That(result[1].Frequency, Is.EqualTo(2)); + Assert.That(result[2].Word, Is.EqualTo("test")); + Assert.That(result[2].Frequency, Is.EqualTo(1)); + } + + [Test] + public void Analyze_AllWordsEqualFrequency_ReturnsCorrectWeight() + { + var words = new[] { "a", "b", "c", "d" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(4)); + foreach (var item in result) + { + Assert.That(item.Weight, Is.EqualTo(1.0).Within(0.001)); + } + } + + [Test] + public void Analyze_DifferentFrequencies_ReturnsCorrectWeights() + { + var words = new[] { "a", "a", "a", "b", "b", "c" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + var itemA = result.First(i => i.Word == "a"); + var itemB = result.First(i => i.Word == "b"); + var itemC = result.First(i => i.Word == "c"); + + Assert.That(itemA.Frequency, Is.EqualTo(3)); + Assert.That(itemB.Frequency, Is.EqualTo(2)); + Assert.That(itemC.Frequency, Is.EqualTo(1)); + + Assert.That(itemA.Weight, Is.EqualTo(1.0).Within(0.001)); + Assert.That(itemB.Weight, Is.EqualTo(0.666).Within(0.001)); + Assert.That(itemC.Weight, Is.EqualTo(0.333).Within(0.001)); + } + + [Test] + public void Analyze_CaseSensitive_ReturnsSeparateItems() + { + var words = new[] { "Hello", "hello", "HELLO" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + Assert.That(result.Count, Is.EqualTo(3)); + } + } +} \ No newline at end of file diff --git a/di.sln b/di.sln index a50991da..9f12a7c7 100644 --- a/di.sln +++ b/di.sln @@ -1,6 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter/di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +# Visual Studio Version 17 +VisualStudioVersion = 17.13.35919.96 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "di", "FractalPainter\di.csproj", "{4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGenerator", "TagCloudGenerator\TagCloudGenerator.csproj", "{7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGeneratorTests", "TagCloudGeneratorTests\TagCloudGeneratorTests.csproj", "{235C2379-72BC-4BF2-994D-B5DA4146AB8D}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -12,5 +19,16 @@ Global {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B70F6B3-5C20-40D2-BFC9-D95C18D65DD6}.Release|Any CPU.Build.0 = Release|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F81F0E1-386F-4ACE-A723-5AEDE6DF34A9}.Release|Any CPU.Build.0 = Release|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal From dca463c4793c397038616466424df1f175d64dd0 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Fri, 12 Dec 2025 03:32:27 +0500 Subject: [PATCH 2/8] Made architecture, client and tests updates and fixes --- .../Algorithms/BasicTagCloudAlgorithm.cs | 7 +- TagCloudGenerator/Clients/ConsoleClient.cs | 324 ++---------------- TagCloudGenerator/Clients/Options.cs | 41 +++ TagCloudGenerator/Clients/UiClient.cs | 7 +- .../Core/Interfaces/IAnalyzer.cs | 10 +- TagCloudGenerator/Core/Interfaces/IClient.cs | 7 +- .../Core/Interfaces/ICloudItem.cs | 21 -- TagCloudGenerator/Core/Interfaces/IFilter.cs | 10 +- .../Core/Interfaces/IFontSizeCalculator.cs | 7 +- TagCloudGenerator/Core/Interfaces/IReader.cs | 5 +- .../Core/Interfaces/IRenderSettings.cs | 23 -- .../Core/Interfaces/IRenderer.cs | 10 +- .../Core/Interfaces/ITagCloudAlgorithm.cs | 7 +- .../Core/Interfaces/ITagCloudGenerator.cs | 9 +- .../Core/Interfaces/ITextMeasurer.cs | 7 +- .../Core/Models/CanvasSettings.cs | 65 ++++ TagCloudGenerator/Core/Models/CloudItem.cs | 15 +- .../Core/Models/RenderSettings.cs | 24 -- TagCloudGenerator/Core/Models/TextSettings.cs | 47 +++ .../Core/Services/CloudGenerator.cs | 72 ++-- TagCloudGenerator/DI/TagCloudModule.cs | 12 +- .../Analyzers/WordsFrequencyAnalyzer.cs | 40 +-- .../Calculators/LinearFontSizeCalculator.cs | 7 +- .../Filters/BoringWordsFilter.cs | 12 +- .../Filters/ToLowerCaseFilter.cs | 14 + .../Measurers/GraphicsTextMeasurer.cs | 14 +- .../Infrastructure/Readers/LineTextReader.cs | 12 +- .../Infrastructure/Renderers/PngRenderer.cs | 46 ++- TagCloudGenerator/TagCloudGenerator.csproj | 3 +- .../BoringWordsFilterTests.cs | 42 +-- TagCloudGeneratorTests/CloudGeneratorTests.cs | 78 +++-- .../GraphicsTextMeasurerTests.cs | 32 +- TagCloudGeneratorTests/LineTextReaderTests.cs | 52 +-- .../LinearFontSizeCalculatorTests.cs | 31 +- .../WordsFrequencyAnalyzerTests.cs | 38 +- 35 files changed, 408 insertions(+), 743 deletions(-) create mode 100644 TagCloudGenerator/Clients/Options.cs delete mode 100644 TagCloudGenerator/Core/Interfaces/ICloudItem.cs delete mode 100644 TagCloudGenerator/Core/Interfaces/IRenderSettings.cs create mode 100644 TagCloudGenerator/Core/Models/CanvasSettings.cs delete mode 100644 TagCloudGenerator/Core/Models/RenderSettings.cs create mode 100644 TagCloudGenerator/Core/Models/TextSettings.cs create mode 100644 TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs index df80da64..75bad782 100644 --- a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Algorithms diff --git a/TagCloudGenerator/Clients/ConsoleClient.cs b/TagCloudGenerator/Clients/ConsoleClient.cs index c0e7349b..964ee029 100644 --- a/TagCloudGenerator/Clients/ConsoleClient.cs +++ b/TagCloudGenerator/Clients/ConsoleClient.cs @@ -1,7 +1,5 @@ -using System; +using CommandLine; using System.Drawing; -using System.IO; -using System.Linq; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; @@ -18,315 +16,67 @@ public ConsoleClient(ITagCloudGenerator generator) public void Run(string[] args) { - Console.WriteLine("=== Tag Cloud Generator ==="); - Console.WriteLine("Type '--help' to see available commands"); - Console.WriteLine(); - - while (true) - { - Console.Write("> "); - var commandLine = Console.ReadLine(); - - if (string.IsNullOrWhiteSpace(commandLine)) - continue; - - if (commandLine == "exit" || commandLine == "quit") - break; - - var commandArgs = ParseCommandLine(commandLine); - - if (commandArgs.Length == 0) - continue; - - if (commandArgs[0] == "--help" || commandArgs[0] == "-h") - { - PrintHelp(); - continue; - } - - var result = ParseArguments(commandArgs); - if (!result.IsValid) - { - Console.WriteLine("Error: Input file is required!"); - Console.WriteLine("Usage: [options]"); - Console.WriteLine("Use --help for more information"); - continue; - } - - if (!File.Exists(result.InputFile)) - { - Console.WriteLine($"Error: File '{result.InputFile}' not found!"); - continue; - } - - Console.WriteLine("\nSettings:"); - PrintSettings(result); - - Console.WriteLine("\nStarting generation..."); - try - { - _generator.Generate(result.InputFile, result.OutputFile, result.RenderSettings); - Console.WriteLine($"\nSuccess! Image saved to bin folder of the project as: {result.OutputFile}"); - } - catch (Exception ex) - { - Console.WriteLine($"\nError: {ex.Message}"); - } - - Console.WriteLine(); - } + Parser.Default.ParseArguments(args) + .WithParsed(RunWithOptions) + .WithNotParsed(errors => { }); } - private string[] ParseCommandLine(string commandLine) + private void RunWithOptions(Options opts) { - var parts = new System.Collections.Generic.List(); - var currentPart = new System.Text.StringBuilder(); - bool inQuotes = false; - - for (int i = 0; i < commandLine.Length; i++) + if (!File.Exists(opts.InputFile)) { - char c = commandLine[i]; - - if (c == '"') - { - inQuotes = !inQuotes; - } - else if (c == ' ' && !inQuotes) - { - if (currentPart.Length > 0) - { - parts.Add(currentPart.ToString()); - currentPart.Clear(); - } - } - else - { - currentPart.Append(c); - } + Console.WriteLine($"Error: input file '{opts.InputFile}' does not exist."); + return; } - if (currentPart.Length > 0) - { - parts.Add(currentPart.ToString()); - } + var canvasSettings = new CanvasSettings() + .SetSize(opts.Width, opts.Height) + .SetBackgroundColor(TryParseColor(opts.BackgroundColor)) + .WithCenterCloud() + .WithShowRectangles() + .SetPadding(opts.Padding); - return parts.ToArray(); - } + var textSettings = new TextSettings() + .SetFontFamily(opts.FontFamily) + .SetFontSizeRange(opts.MinFontSize, opts.MaxFontSize) + .SetTextColor(TryParseColor(opts.TextColor)); - private ParseResult ParseArguments(string[] args) - { - var result = new ParseResult - { - RenderSettings = new RenderSettings - { - FontFamily = "Arial", - MinFontSize = 12f, - MaxFontSize = 72f, - BackgroundColor = Color.White, - TextColor = Color.Black, - CanvasSize = new Size(800, 600), - CenterCloud = true, - ShowRectangles = false, - Padding = 50 - } - }; + string inputFile = opts.InputFile; + string outputFile = opts.OutputFile; - string inputFile = null; - for (int i = 0; i < args.Length; i++) - { - if (!args[i].StartsWith("-") && !args[i].StartsWith("--")) - { - if (inputFile == null && args[i].EndsWith(".txt", StringComparison.OrdinalIgnoreCase)) - { - inputFile = args[i]; - } - } - } + Console.WriteLine("Starting tag cloud generation..."); + Console.WriteLine($"Input file: {inputFile}"); + Console.WriteLine($"Output file: {outputFile}"); - if (inputFile == null) + try { - result.IsValid = false; - return result; + _generator.Generate(inputFile, outputFile, canvasSettings, textSettings); + Console.WriteLine("Tag cloud generation completed successfully!"); } - - result.InputFile = inputFile; - result.OutputFile = GenerateOutputFilename(inputFile); - result.IsValid = true; - - for (int i = 0; i < args.Length; i++) + catch (Exception ex) { - switch (args[i].ToLower()) - { - case "--font": - case "-f": - if (i + 1 < args.Length && !IsFlag(args[i + 1])) - { - result.RenderSettings.FontFamily = args[++i]; - } - break; - - case "--background": - case "-bg": - if (i + 1 < args.Length && !IsFlag(args[i + 1])) - { - result.RenderSettings.BackgroundColor = ParseColor(args[++i], result.RenderSettings.BackgroundColor); - } - break; - - case "--text-color": - case "-tc": - if (i + 1 < args.Length && !IsFlag(args[i + 1])) - { - result.RenderSettings.TextColor = ParseColor(args[++i], result.RenderSettings.TextColor); - } - break; - - case "--width": - case "-w": - if (i + 1 < args.Length && int.TryParse(args[i + 1], out int width)) - { - result.RenderSettings.CanvasSize = new Size(width, result.RenderSettings.CanvasSize.Height); - i++; - } - break; - - case "--height": - if (i + 1 < args.Length && int.TryParse(args[i + 1], out int height)) - { - result.RenderSettings.CanvasSize = new Size(result.RenderSettings.CanvasSize.Width, height); - i++; - } - break; - - case "--show-rectangles": - result.RenderSettings.ShowRectangles = true; - break; - - case "--output": - case "-o": - if (i + 1 < args.Length && !IsFlag(args[i + 1])) - { - result.OutputFile = args[++i]; - if (!result.OutputFile.EndsWith(".png", StringComparison.OrdinalIgnoreCase)) - result.OutputFile += ".png"; - } - break; - } + Console.WriteLine($"Error during generation: {ex.Message}"); } - - return result; } - private bool IsFlag(string arg) + private static Color? TryParseColor(string? colorStr) { - return arg.StartsWith("-") || arg.StartsWith("--"); - } - - private string GenerateOutputFilename(string inputFile) - { - string fileName = Path.GetFileNameWithoutExtension(inputFile); - string directory = Path.Combine(Path.GetDirectoryName(inputFile), "CLouds"); - Directory.CreateDirectory(directory); - - string timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss"); - string outputFile = $"{fileName}_cloud_{timestamp}.png"; - - if (!string.IsNullOrEmpty(directory)) - { - outputFile = Path.Combine(directory, outputFile); - } - - return outputFile; - } + if (string.IsNullOrWhiteSpace(colorStr)) + return null; - private Color ParseColor(string colorString, Color defaultColor) - { try { - var color = Color.FromName(colorString); - if (color.IsKnownColor) - return color; - - if (colorString.StartsWith("#")) - { - return ColorTranslator.FromHtml(colorString); - } - - if (colorString.Contains(",")) - { - var parts = colorString.Split(','); - if (parts.Length == 3) - { - return Color.FromArgb( - int.Parse(parts[0].Trim()), - int.Parse(parts[1].Trim()), - int.Parse(parts[2].Trim())); - } - } + var known = Color.FromName(colorStr); + if (known.IsKnownColor) + return known; - return defaultColor; + return ColorTranslator.FromHtml(colorStr); } catch { - return defaultColor; + Console.WriteLine($"Color '{colorStr}' could not be parsed. Default color will be used."); + return null; } } - - private void PrintHelp() - { - Console.WriteLine("=== Tag Cloud Generator Help ==="); - Console.WriteLine(); - Console.WriteLine("Usage:"); - Console.WriteLine(" [options]"); - Console.WriteLine(" --help"); - Console.WriteLine(); - Console.WriteLine("Arguments:"); - Console.WriteLine(" Input text file (required)"); - Console.WriteLine(); - Console.WriteLine("Options:"); - Console.WriteLine(" -f, --font Font family (default: Arial)"); - Console.WriteLine(" -bg, --background Background color (default: White)"); - Console.WriteLine(" -tc, --text-color Text color (default: Black)"); - Console.WriteLine(" -w, --width Canvas width (default: 800)"); - Console.WriteLine(" --height Canvas height (default: 600)"); - Console.WriteLine(" -o, --output Output file (default: _cloud_.png)"); - Console.WriteLine(" --show-rectangles Show word rectangles (debug mode)"); - Console.WriteLine(" --help Show this help message"); - Console.WriteLine(); - Console.WriteLine("Color formats:"); - Console.WriteLine(" - Named colors: White, Black, Red, Blue, Green, etc."); - Console.WriteLine(" - HEX: #FFFFFF, #000000, #FF0000"); - Console.WriteLine(" - RGB: 255,255,255 or 0,0,0"); - Console.WriteLine(); - Console.WriteLine("Examples:"); - Console.WriteLine(" document.txt"); - Console.WriteLine(" words.txt --font \"Times New Roman\""); - Console.WriteLine(" input.txt -bg White -tc Navy --width 1200 --height 800"); - Console.WriteLine(" text.txt --background #F0F0F0 --text-color #333333 -o cloud.png"); - Console.WriteLine(" data.txt --show-rectangles"); - Console.WriteLine(); - Console.WriteLine("Commands:"); - Console.WriteLine(" --help Show this help"); - Console.WriteLine(" exit, quit Exit program"); - } - - private void PrintSettings(ParseResult result) - { - Console.WriteLine($" Input file: {result.InputFile}"); - Console.WriteLine($" Output file: {result.OutputFile}"); - Console.WriteLine($" Font: {result.RenderSettings.FontFamily}"); - Console.WriteLine($" Background: {result.RenderSettings.BackgroundColor.Name}"); - Console.WriteLine($" Text color: {result.RenderSettings.TextColor.Name}"); - Console.WriteLine($" Canvas size: {result.RenderSettings.CanvasSize.Width}x{result.RenderSettings.CanvasSize.Height}"); - Console.WriteLine($" Show rectangles: {result.RenderSettings.ShowRectangles}"); - } - - private class ParseResult - { - public bool IsValid { get; set; } - public string InputFile { get; set; } - public string OutputFile { get; set; } - public RenderSettings RenderSettings { get; set; } - } } } \ No newline at end of file diff --git a/TagCloudGenerator/Clients/Options.cs b/TagCloudGenerator/Clients/Options.cs new file mode 100644 index 00000000..30aac4f0 --- /dev/null +++ b/TagCloudGenerator/Clients/Options.cs @@ -0,0 +1,41 @@ +using CommandLine; + +namespace TagCloudGenerator.Clients +{ + public class Options + { + [Value(0, MetaName = "input", + HelpText = "Path to the input file containing text.", + Required = true)] + public string InputFile { get; set; } + + [Value(1, MetaName = "output", + HelpText = "Path for the output image (e.g., out.png).", + Required = true)] + public string OutputFile { get; set; } + + [Option("width", HelpText = "Canvas width (default is 1000).")] + public int? Width { get; set; } + + [Option("height", HelpText = "Canvas height (default is 1000).")] + public int? Height { get; set; } + + [Option("padding", HelpText = "Canvas padding (default if 50).")] + public int? Padding { get; set; } + + [Option("bgcolor", HelpText = "Background color (name or #RRGGBB).")] + public string BackgroundColor { get; set; } + + [Option("textcolor", HelpText = "Text color (name or #RRGGBB).")] + public string TextColor { get; set; } + + [Option("font", HelpText = "Font family name (e.g., Arial).")] + public string FontFamily { get; set; } + + [Option("minsize", HelpText = "Minimum font size.")] + public float? MinFontSize { get; set; } + + [Option("maxsize", HelpText = "Maximum font size.")] + public float? MaxFontSize { get; set; } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Clients/UiClient.cs b/TagCloudGenerator/Clients/UiClient.cs index 28dce0e5..92e5eafd 100644 --- a/TagCloudGenerator/Clients/UiClient.cs +++ b/TagCloudGenerator/Clients/UiClient.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Clients { diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs index 232ab84f..de6b87e0 100644 --- a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Core.Interfaces { public interface IAnalyzer { - IEnumerable Analyze(IEnumerable words); + List Analyze(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs index 32b64b0c..20ce48af 100644 --- a/TagCloudGenerator/Core/Interfaces/IClient.cs +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace TagCloudGenerator.Core.Interfaces { public interface IClient diff --git a/TagCloudGenerator/Core/Interfaces/ICloudItem.cs b/TagCloudGenerator/Core/Interfaces/ICloudItem.cs deleted file mode 100644 index 7d8b387c..00000000 --- a/TagCloudGenerator/Core/Interfaces/ICloudItem.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TagCloudGenerator.Core.Interfaces -{ - public interface ICloudItem - { - string Word { get; } - Rectangle Rectangle { get; } - float FontSize { get; } - Color? Color { get; } - string FontFamily { get; } - FontStyle FontStyle { get; } - int Frequency { get; } - double Weight { get; } - } -} diff --git a/TagCloudGenerator/Core/Interfaces/IFilter.cs b/TagCloudGenerator/Core/Interfaces/IFilter.cs index ed65c180..e660e782 100644 --- a/TagCloudGenerator/Core/Interfaces/IFilter.cs +++ b/TagCloudGenerator/Core/Interfaces/IFilter.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace TagCloudGenerator.Core.Interfaces { public interface IFilter { - IEnumerable Filter(IEnumerable words); - bool ShouldInclude(string word); + List Filter(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs index a61fabab..57033c7e 100644 --- a/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs +++ b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace TagCloudGenerator.Core.Interfaces { public interface IFontSizeCalculator diff --git a/TagCloudGenerator/Core/Interfaces/IReader.cs b/TagCloudGenerator/Core/Interfaces/IReader.cs index a01af628..7890629d 100644 --- a/TagCloudGenerator/Core/Interfaces/IReader.cs +++ b/TagCloudGenerator/Core/Interfaces/IReader.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace TagCloudGenerator.Core.Interfaces { public interface IReader { - IEnumerable TryRead(string filePath); + List TryRead(string filePath); } } diff --git a/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs b/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs deleted file mode 100644 index 9970b529..00000000 --- a/TagCloudGenerator/Core/Interfaces/IRenderSettings.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace TagCloudGenerator.Core.Interfaces -{ - public interface IRenderSettings - { - string OutputPath { get; set; } - string FontFamily { get; set; } - float MinFontSize { get; set; } - float MaxFontSize { get; set; } - Color BackgroundColor { get; set; } - Color TextColor { get; set; } - Size CanvasSize { get; set; } - bool CenterCloud { get; set; } - bool ShowRectangles { get; set; } - int Padding { get; set; } - } -} diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs index e9f9c4cb..452b9884 100644 --- a/TagCloudGenerator/Core/Interfaces/IRenderer.cs +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Core.Interfaces { public interface IRenderer { - void Render(IEnumerable items, RenderSettings settings); + void Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings, string outputFile); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs index 833627b0..3f7a7ad1 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; namespace TagCloudGenerator.Core.Interfaces { diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs index e1a8b970..25feb60a 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -1,14 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudGenerator { - public void Generate(string inputFile, string outputFile, RenderSettings settings); + public void Generate(string inputFile, string outputFile, CanvasSettings canvasSettings, TextSettings textSettings); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs index f29aca0e..0f1f73be 100644 --- a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs +++ b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; namespace TagCloudGenerator.Core.Interfaces { diff --git a/TagCloudGenerator/Core/Models/CanvasSettings.cs b/TagCloudGenerator/Core/Models/CanvasSettings.cs new file mode 100644 index 00000000..eab79127 --- /dev/null +++ b/TagCloudGenerator/Core/Models/CanvasSettings.cs @@ -0,0 +1,65 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class CanvasSettings + { + public Color BackgroundColor { get; set; } = Color.White; + public Size CanvasSize { get; set; } = new Size(1000, 1000); + public bool CenterCloud { get; set; } = true; + public bool ShowRectangles { get; set; } = true; + public int Padding { get; set; } = 50; + + public CanvasSettings SetWidth(int? width) + { + if (width.HasValue && width > 0) + CanvasSize = new Size(width.Value, CanvasSize.Height); + return this; + } + + public CanvasSettings SetHeight(int? height) + { + if (height.HasValue && height > 0) + CanvasSize = new Size(CanvasSize.Width, height.Value); + return this; + } + + public CanvasSettings SetSize(int? width, int? height) + { + if (width.HasValue && height.HasValue && width > 0 && height > 0) + CanvasSize = new Size(width.Value, height.Value); + else if (width.HasValue && width > 0) + CanvasSize = new Size(width.Value, CanvasSize.Height); + else if (height.HasValue && height > 0) + CanvasSize = new Size(CanvasSize.Width, height.Value); + + return this; + } + + public CanvasSettings SetBackgroundColor(Color? color) + { + if (color.HasValue) + BackgroundColor = color.Value; + return this; + } + + public CanvasSettings WithCenterCloud(bool value = true) + { + CenterCloud = value; + return this; + } + + public CanvasSettings WithShowRectangles(bool value = true) + { + ShowRectangles = value; + return this; + } + + public CanvasSettings SetPadding(int? padding) + { + if (padding.HasValue && padding >= 0) + Padding = (int)padding; + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Models/CloudItem.cs b/TagCloudGenerator/Core/Models/CloudItem.cs index 7f3674ce..8dc368b7 100644 --- a/TagCloudGenerator/Core/Models/CloudItem.cs +++ b/TagCloudGenerator/Core/Models/CloudItem.cs @@ -1,14 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; +using System.Drawing; namespace TagCloudGenerator.Core.Models { - public class CloudItem : ICloudItem + public class CloudItem { public string Word { get; } public Rectangle Rectangle { get; } @@ -39,11 +33,6 @@ public CloudItem( Weight = weight; } - public static CloudItem Create(string word, Rectangle rectangle, float fontSize) - { - return new CloudItem(word, rectangle, fontSize); - } - public CloudItem WithRectangle(Rectangle newRectangle) { return new CloudItem(Word, newRectangle, FontSize, Color, FontFamily, FontStyle, Frequency, Weight); diff --git a/TagCloudGenerator/Core/Models/RenderSettings.cs b/TagCloudGenerator/Core/Models/RenderSettings.cs deleted file mode 100644 index b7ab9e6b..00000000 --- a/TagCloudGenerator/Core/Models/RenderSettings.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; - -namespace TagCloudGenerator.Core.Models -{ - public class RenderSettings : IRenderSettings - { - public string OutputPath { get; set; } - public string FontFamily { get; set; } = "Arial"; - public float MinFontSize { get; set; } = 12f; - public float MaxFontSize { get; set; } = 72f; - public Color BackgroundColor { get; set; } - public Color TextColor { get; set; } - public Size CanvasSize { get; set; } - public bool CenterCloud { get; set; } - public bool ShowRectangles { get; set; } - public int Padding { get; set; } = 50; - } -} diff --git a/TagCloudGenerator/Core/Models/TextSettings.cs b/TagCloudGenerator/Core/Models/TextSettings.cs new file mode 100644 index 00000000..53a35b8e --- /dev/null +++ b/TagCloudGenerator/Core/Models/TextSettings.cs @@ -0,0 +1,47 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class TextSettings + { + public string FontFamily { get; set; } = "Arial"; + public float MinFontSize { get; set; } = 12f; + public float MaxFontSize { get; set; } = 72f; + public Color TextColor { get; set; } = Color.Black; + + public TextSettings SetFontFamily(string? font) + { + if (!string.IsNullOrWhiteSpace(font)) + FontFamily = font; + return this; + } + + public TextSettings SetMinFontSize(float? size) + { + if (size.HasValue && size.Value >= 0) + MinFontSize = size.Value; + return this; + } + + public TextSettings SetMaxFontSize(float? size) + { + if (size.HasValue && size.Value >= 0) + MaxFontSize = size.Value; + return this; + } + + public TextSettings SetFontSizeRange(float? minSize, float? maxSize) + { + SetMinFontSize(minSize); + SetMaxFontSize(maxSize); + return this; + } + + public TextSettings SetTextColor(Color? color) + { + if (color.HasValue) + TextColor = color.Value; + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index 6f53bcf9..9e693756 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; @@ -38,32 +33,27 @@ public CloudGenerator(ITagCloudAlgorithm algorithm, _center = new Point(0, 0); } - public void Generate(string inputFile, string outputFile, RenderSettings renderSettings) + public void Generate(string inputFile, string outputFile, CanvasSettings canvasSettings, TextSettings textSettings) { - var words = _reader.TryRead(inputFile); - if (words == null || !words.Any()) - { + var words = _reader.TryRead(inputFile).ToList(); + + if (words == null || words.Count == 0) return; - } words = ApplyFilters(words); - if (!words.Any()) - { + if(words.Count == 0) return; - } var cloudItems = _analyzer.Analyze(words).ToList(); - var preparedItems = PrepareCloudItems(cloudItems, renderSettings).ToList(); + var preparedItems = PrepareCloudItems(cloudItems, textSettings).ToList(); var arrangedItems = ArrangeCloudItems(preparedItems).ToList(); - renderSettings.OutputPath = outputFile; - - _renderer.Render(arrangedItems, renderSettings); + _renderer.Render(arrangedItems, canvasSettings, textSettings, outputFile); } - private IEnumerable ApplyFilters(IEnumerable words) + private List ApplyFilters(List words) { var filteredWords = words; foreach (var filter in _filters) @@ -73,17 +63,15 @@ private IEnumerable ApplyFilters(IEnumerable words) return filteredWords; } - private IEnumerable PrepareCloudItems(IEnumerable items, RenderSettings settings) + private IEnumerable PrepareCloudItems(IEnumerable items, TextSettings settings) { - var itemsList = items.ToList(); - var frequencies = itemsList.Select(i => i.Frequency).ToList(); - var minFrequency = frequencies.Min(); - var maxFrequency = frequencies.Max(); + var itemsList = new List(); - for (int i = 0; i < itemsList.Count; i++) - { - var item = itemsList[i]; + var minFrequency = items.Min(i => i.Frequency); + var maxFrequency = items.Max(i => i.Frequency); + foreach (var item in items) + { var fontSize = _fontSizeCalculator.Calculate( item.Frequency, minFrequency, @@ -96,26 +84,24 @@ private IEnumerable PrepareCloudItems(IEnumerable items, R fontSize, settings.FontFamily); - yield return new CloudItem( - word: item.Word, - rectangle: new Rectangle(Point.Empty, textSize), - fontSize: fontSize, - color: settings.TextColor, - fontFamily: settings.FontFamily, - frequency: item.Frequency, - weight: item.Weight - ); + itemsList.Add( + new CloudItem( + word: item.Word, + rectangle: new Rectangle(Point.Empty, textSize), + fontSize: fontSize, + color: settings.TextColor, + fontFamily: settings.FontFamily, + frequency: item.Frequency, + weight: item.Weight + )); } + return itemsList; } - private IEnumerable ArrangeCloudItems(IEnumerable items) + private List ArrangeCloudItems(List items) { - - foreach (var item in items) - { - var newRectangle = _algorithm.PutNextRectangle(item.Rectangle.Size); - yield return item.WithRectangle(newRectangle); - } + return items.Select(item => item.WithRectangle(_algorithm.PutNextRectangle(item.Rectangle.Size))) + .ToList(); } } } diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs index 40277cd1..aad1b83f 100644 --- a/TagCloudGenerator/DI/TagCloudModule.cs +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -1,14 +1,7 @@ using Autofac; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; using TagCloudGenerator.Algorithms; using TagCloudGenerator.Clients; using TagCloudGenerator.Core.Interfaces; -using TagCloudGenerator.Core.Models; using TagCloudGenerator.Core.Services; using TagCloudGenerator.Infrastructure.Analyzers; using TagCloudGenerator.Infrastructure.Calculators; @@ -26,13 +19,12 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As(); builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType() - .As() - .WithParameter("minFontSize", 12f) - .WithParameter("maxFontSize", 72f); + .As(); builder.RegisterType() .As(); diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index cc099b30..e106f660 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; @@ -11,27 +6,28 @@ namespace TagCloudGenerator.Infrastructure.Analyzers { public class WordsFrequencyAnalyzer : IAnalyzer { - public IEnumerable Analyze(IEnumerable words) + public List Analyze(List words) { + if (words == null || words.Count == 0) + return new List(); + var wordGroups = words .GroupBy(word => word) - .Select(group => new - { - Word = group.Key, - Frequency = group.Count() - }) + .Select(group => new { Word = group.Key, Frequency = group.Count() }) .OrderByDescending(x => x.Frequency); - foreach (var group in wordGroups) - { - yield return new CloudItem( - word: group.Word, - rectangle: Rectangle.Empty, - fontSize: 0, - frequency: group.Frequency, - weight: (double)group.Frequency / wordGroups.Max(g => g.Frequency) - ); - } + if (wordGroups.Count() == 0) + return new List(); + + var maxFreq = wordGroups.Max(g => g.Frequency); + + return wordGroups.Select(group => new CloudItem( + word: group.Word, + rectangle: Rectangle.Empty, + fontSize: 0, + frequency: group.Frequency, + weight: (double)group.Frequency / maxFreq + )).ToList(); } } } diff --git a/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs index 8b90b15b..8fc0ed54 100644 --- a/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs +++ b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Infrastructure.Calculators { diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs index 7b45c49f..ae05d2b5 100644 --- a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -1,18 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Infrastructure.Filters { public class BoringWordsFilter : IFilter { private readonly string[] _boringWords = ["in", "it", "a", "as", "for", "of", "on"]; - public IEnumerable Filter(IEnumerable words) + public List Filter(List words) { - return words.Where(w => ShouldInclude(w)); + if (words == null || words.Count() == 0) return new List(); + return words.Where(w => ShouldInclude(w)).ToList(); } public bool ShouldInclude(string word) diff --git a/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs b/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs new file mode 100644 index 00000000..43d48a6e --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs @@ -0,0 +1,14 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Filters +{ + public class ToLowerCaseFilter : IFilter + { + public List Filter(List words) + { + if (words == null || words.Count() == 0) return new List(); + + return words.Select(w => w.ToLower()).ToList(); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs index 390be0fa..5ecdd40c 100644 --- a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs +++ b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs @@ -1,22 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Drawing; using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Infrastructure.Measurers { public class GraphicsTextMeasurer : ITextMeasurer { - private readonly string _fontFamily; - - public GraphicsTextMeasurer(string fontFamily = "Arial") - { - _fontFamily = fontFamily; - } - public Size Measure(string word, float fontSize, string fontFamily) { using var font = new Font(fontFamily, fontSize); diff --git a/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs b/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs index 000f6d96..b1999e04 100644 --- a/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs @@ -1,20 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Infrastructure.Reader { public class LineTextReader : IReader { - public IEnumerable? TryRead(string filePath) + public List TryRead(string filePath) { try { - string[] fileContent = File.ReadAllLines(filePath); - return fileContent.Select(w => w.ToLower()); + return File.ReadAllLines(filePath).ToList(); } catch(IOException e) { diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index 619dc832..27833fcb 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -1,46 +1,44 @@ -using System; -using System.Collections.Generic; +using System.Drawing; using System.Drawing.Imaging; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; -using System.IO; namespace TagCloudGenerator.Infrastructure.Renderers { public class PngRenderer : IRenderer { - public void Render(IEnumerable items, RenderSettings settings) + public void Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings, string outputFile) { var itemsList = items.ToList(); - if (!itemsList.Any()) + if (items.Count() == 0) { Console.WriteLine("No elements to render"); return; } - using var bitmap = new Bitmap(settings.CanvasSize.Width, settings.CanvasSize.Height); + using var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); using var graphics = Graphics.FromImage(bitmap); ConfigureGraphics(graphics); - graphics.Clear(settings.BackgroundColor); + graphics.Clear(canvasSettings.BackgroundColor); - var (offsetX, offsetY) = CalculateOffset(itemsList, settings); + var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); + + using var brush = new SolidBrush(textSettings.TextColor); + using var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using var pen = new Pen(textSettings.TextColor, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; foreach (var item in itemsList) { - DrawCloudItem(graphics, item, offsetX, offsetY, settings); + DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); } - bitmap.Save(settings.OutputPath, ImageFormat.Png); + bitmap.Save(outputFile, ImageFormat.Png); } private (int offsetX, int offsetY) CalculateOffset( List items, - RenderSettings settings) + CanvasSettings settings) { if (!settings.CenterCloud) return (settings.Padding, settings.Padding); @@ -64,7 +62,11 @@ private void DrawCloudItem( CloudItem item, int offsetX, int offsetY, - RenderSettings settings) + CanvasSettings canvasSettings, + TextSettings textSettings, + SolidBrush brush, + Pen pen, + StringFormat stringFormat) { var drawRect = new Rectangle( item.Rectangle.X + offsetX, @@ -72,7 +74,7 @@ private void DrawCloudItem( item.Rectangle.Width, item.Rectangle.Height); - var color = item.Color ?? settings.TextColor; + var color = item.Color ?? textSettings.TextColor; using var font = new Font( item.FontFamily, @@ -80,18 +82,10 @@ private void DrawCloudItem( item.FontStyle, GraphicsUnit.Pixel); - using var brush = new SolidBrush(color); - using var stringFormat = new StringFormat - { - Alignment = StringAlignment.Center, - LineAlignment = StringAlignment.Center - }; - graphics.DrawString(item.Word, font, brush, drawRect, stringFormat); - if (settings.ShowRectangles) + if (canvasSettings.ShowRectangles) { - using var pen = new Pen(color, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; graphics.DrawRectangle(pen, drawRect); } } diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj index 15081f91..e21897b5 100644 --- a/TagCloudGenerator/TagCloudGenerator.csproj +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -4,11 +4,12 @@ Exe net8.0 enable - enable + disable + diff --git a/TagCloudGeneratorTests/BoringWordsFilterTests.cs b/TagCloudGeneratorTests/BoringWordsFilterTests.cs index 29c1f109..52232f6a 100644 --- a/TagCloudGeneratorTests/BoringWordsFilterTests.cs +++ b/TagCloudGeneratorTests/BoringWordsFilterTests.cs @@ -1,36 +1,24 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NUnit.Framework; -using System.Linq; +using NUnit.Framework; using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudGeneratorTests { public class BoringWordsFilterTests { - private BoringWordsFilter filter; - - [SetUp] - public void Setup() - { - filter = new BoringWordsFilter(); - } + private BoringWordsFilter filter = new BoringWordsFilter(); [Test] public void Filter_EmptyInput_ReturnsEmpty() { - var words = Enumerable.Empty(); + var words = new List(); var result = filter.Filter(words); Assert.That(result, Is.Empty); } [Test] - public void Filter_RemovesBoringWords() + public void Filter_RemovesBoringWords_Test() { - var words = new[] { "hello", "in", "world", "a", "test" }; + var words = new List { "hello", "in", "world", "a", "test" }; var result = filter.Filter(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); @@ -42,17 +30,17 @@ public void Filter_RemovesBoringWords() } [Test] - public void Filter_AllBoringWords_ReturnsEmpty() + public void Filter_AllBoringWords_ReturnsEmpty_Test() { - var words = new[] { "in", "a", "for", "on" }; + var words = new List { "in", "a", "for", "on" }; var result = filter.Filter(words); Assert.That(result, Is.Empty); } [Test] - public void Filter_NoBoringWords_ReturnsAllWords() + public void Filter_NoBoringWords_ReturnsAllWords_Test() { - var words = new[] { "hello", "world", "test" }; + var words = new List { "hello", "world", "test" }; var result = filter.Filter(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); @@ -60,9 +48,9 @@ public void Filter_NoBoringWords_ReturnsAllWords() } [Test] - public void ShouldInclude_ReturnsFalseForBoringWords() + public void ShouldInclude_ReturnsFalseForBoringWords_Test() { - var boringWords = new[] { "in", "it", "a", "as", "for", "of", "on" }; + var boringWords = new List { "in", "it", "a", "as", "for", "of", "on" }; foreach (var word in boringWords) { Assert.That(filter.ShouldInclude(word), Is.False); @@ -70,9 +58,9 @@ public void ShouldInclude_ReturnsFalseForBoringWords() } [Test] - public void ShouldInclude_ReturnsTrueForNormalWords() + public void ShouldInclude_ReturnsTrueForNormalWords_Test() { - var normalWords = new[] { "hello", "world", "computer", "programming", "test" }; + var normalWords = new List { "hello", "world", "computer", "programming", "test" }; foreach (var word in normalWords) { Assert.That(filter.ShouldInclude(word), Is.True); @@ -80,9 +68,9 @@ public void ShouldInclude_ReturnsTrueForNormalWords() } [Test] - public void Filter_PreservesOrder() + public void Filter_PreservesOrder_Test() { - var words = new[] { "hello", "in", "world", "a", "test", "for" }; + var words = new List { "hello", "in", "world", "a", "test", "for" }; var result = filter.Filter(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index 402f87b3..3ddb96d1 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -1,18 +1,10 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Moq; +using Moq; using NUnit.Framework; -using System.Collections.Generic; using System.Drawing; -using System.Linq; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; using TagCloudGenerator.Core.Services; - namespace TagCloudGeneratorTests { public class CloudGeneratorTests @@ -48,49 +40,58 @@ public void Setup() } [Test] - public void Generate_EmptyFile_DoesNothing() + public void Generate_EmptyFile_DoesNothing_Test() { - var words = Enumerable.Empty(); - readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(words); + readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(new List()); - cloudGenerator.Generate("input.txt", "output.png", new RenderSettings()); + cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), new TextSettings()); - rendererMock.Verify(r => r.Render(It.IsAny>(), It.IsAny()), Times.Never); + rendererMock.Verify(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); } [Test] - public void Generate_AllWordsFilteredOut_DoesNothing() + public void Generate_AllWordsFilteredOut_DoesNothing_Test() { - var words = new[] { "in", "a", "for" }; + var words = new List { "in", "a", "for" }; readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(words); - filterMock.Setup(f => f.Filter(It.IsAny>())).Returns(Enumerable.Empty()); + filterMock.Setup(f => f.Filter(It.IsAny>())).Returns(new List()); - cloudGenerator.Generate("input.txt", "output.png", new RenderSettings()); + cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), new TextSettings()); - rendererMock.Verify(r => r.Render(It.IsAny>(), It.IsAny()), Times.Never); + rendererMock.Verify(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never); } [Test] - public void Generate_NormalFlow_CallsAllDependencies() + public void Generate_NormalFlow_CallsAllDependencies_Test() { - var words = new[] { "hello", "world", "test" }; - var filteredWords = new[] { "hello", "world", "test" }; - var cloudItems = new[] + var words = new List { "hello", "world", "test" }; + var filteredWords = new List { "hello", "world", "test" }; + var cloudItems = new List { new CloudItem("hello", Rectangle.Empty, 0, frequency: 2), new CloudItem("world", Rectangle.Empty, 0, frequency: 1) }; - var renderSettings = new RenderSettings - { - MinFontSize = 12f, - MaxFontSize = 72f, - FontFamily = "Arial" - }; readerMock.Setup(r => r.TryRead("input.txt")).Returns(words); filterMock.Setup(f => f.Filter(words)).Returns(filteredWords); analyzerMock.Setup(a => a.Analyze(filteredWords)).Returns(cloudItems); + var textSettings = new TextSettings + { + FontFamily = "Arial", + MinFontSize = 12f, + MaxFontSize = 72f, + TextColor = Color.Black + }; + fontSizeCalculatorMock.Setup(f => f.Calculate(2, 1, 2, 12f, 72f)).Returns(50f); fontSizeCalculatorMock.Setup(f => f.Calculate(1, 1, 2, 12f, 72f)).Returns(20f); @@ -100,15 +101,26 @@ public void Generate_NormalFlow_CallsAllDependencies() algorithmMock.Setup(a => a.PutNextRectangle(new Size(100, 30))).Returns(new Rectangle(0, 0, 100, 30)); algorithmMock.Setup(a => a.PutNextRectangle(new Size(80, 25))).Returns(new Rectangle(100, 0, 80, 25)); - cloudGenerator.Generate("input.txt", "output.png", renderSettings); + cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), textSettings); readerMock.Verify(r => r.TryRead("input.txt"), Times.Once); filterMock.Verify(f => f.Filter(words), Times.Once); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); - fontSizeCalculatorMock.Verify(f => f.Calculate(It.IsAny(), 1, 2, 12f, 72f), Times.Exactly(2)); - textMeasurerMock.Verify(t => t.Measure(It.IsAny(), It.IsAny(), "Arial"), Times.Exactly(2)); + + fontSizeCalculatorMock.Verify(f => + f.Calculate(It.IsAny(), 1, 2, 12f, 72f), Times.Exactly(2)); + textMeasurerMock.Verify(t => + t.Measure(It.IsAny(), It.IsAny(), "Arial"), Times.Exactly(2)); algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); - rendererMock.Verify(r => r.Render(It.IsAny>(), It.Is(s => s.OutputPath == "output.png")), Times.Once); + + rendererMock.Verify(r => r.Render( + It.Is>(items => items.Count() == 2), + It.IsAny(), + It.Is(ts => + ts.FontFamily == "Arial" && + ts.MinFontSize == 12f && + ts.MaxFontSize == 72f), + "output.png"), Times.Once); } } } diff --git a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs index 42ebd96d..ecfa6b63 100644 --- a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs +++ b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs @@ -1,26 +1,14 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NUnit.Framework; -using System.Drawing; +using NUnit.Framework; using TagCloudGenerator.Infrastructure.Measurers; namespace TagCloudGeneratorTests { public class GraphicsTextMeasurerTests { - private GraphicsTextMeasurer measurer; - - [SetUp] - public void Setup() - { - measurer = new GraphicsTextMeasurer("Arial"); - } + private GraphicsTextMeasurer measurer = new GraphicsTextMeasurer(); [Test] - public void Measure_EmptyString_ReturnsValidSize() + public void Measure_EmptyString_ReturnsValidSize_Test() { var result = measurer.Measure("", 12f, "Arial"); Assert.That(result.Width, Is.GreaterThanOrEqualTo(0)); @@ -28,7 +16,7 @@ public void Measure_EmptyString_ReturnsValidSize() } [Test] - public void Measure_SingleCharacter_ReturnsNonZeroSize() + public void Measure_SingleCharacter_ReturnsNonZeroSize_Test() { var result = measurer.Measure("A", 12f, "Arial"); Assert.That(result.Width, Is.GreaterThan(0)); @@ -36,7 +24,7 @@ public void Measure_SingleCharacter_ReturnsNonZeroSize() } [Test] - public void Measure_LongerWord_ReturnsWiderSize() + public void Measure_LongerWord_ReturnsWiderSize_Test() { var shortSize = measurer.Measure("A", 12f, "Arial"); var longSize = measurer.Measure("ABCDEFGHIJ", 12f, "Arial"); @@ -44,7 +32,7 @@ public void Measure_LongerWord_ReturnsWiderSize() } [Test] - public void Measure_LargerFont_ReturnsLargerSize() + public void Measure_LargerFont_ReturnsLargerSize_Test() { var smallSize = measurer.Measure("Test", 12f, "Arial"); var largeSize = measurer.Measure("Test", 24f, "Arial"); @@ -53,7 +41,7 @@ public void Measure_LargerFont_ReturnsLargerSize() } [Test] - public void Measure_DifferentFontFamily_ReturnsDifferentSize() + public void Measure_DifferentFontFamily_ReturnsDifferentSize_Test() { var arialSize = measurer.Measure("Test", 12f, "Arial"); var timesSize = measurer.Measure("Test", 12f, "Times New Roman"); @@ -62,7 +50,7 @@ public void Measure_DifferentFontFamily_ReturnsDifferentSize() } [Test] - public void Measure_SameParametersTwice_ReturnsSameSize() + public void Measure_SameParametersTwice_ReturnsSameSize_Test() { var size1 = measurer.Measure("Consistent", 16f, "Arial"); var size2 = measurer.Measure("Consistent", 16f, "Arial"); @@ -71,7 +59,7 @@ public void Measure_SameParametersTwice_ReturnsSameSize() } [Test] - public void Measure_WithSpaces_ReturnsCorrectSize() + public void Measure_WithSpaces_ReturnsCorrectSize_Test() { var sizeWithoutSpace = measurer.Measure("HelloWorld", 12f, "Arial"); var sizeWithSpace = measurer.Measure("Hello World", 12f, "Arial"); @@ -79,7 +67,7 @@ public void Measure_WithSpaces_ReturnsCorrectSize() } [Test] - public void Measure_VeryLargeFont_ReturnsProportionalSize() + public void Measure_VeryLargeFont_ReturnsProportionalSize_Test() { var result = measurer.Measure("Test", 100f, "Arial"); Assert.That(result.Width, Is.GreaterThan(50)); diff --git a/TagCloudGeneratorTests/LineTextReaderTests.cs b/TagCloudGeneratorTests/LineTextReaderTests.cs index cad4d900..f71f42a6 100644 --- a/TagCloudGeneratorTests/LineTextReaderTests.cs +++ b/TagCloudGeneratorTests/LineTextReaderTests.cs @@ -1,27 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -using NUnit.Framework; -using System.IO; -using System.Linq; +using NUnit.Framework; using TagCloudGenerator.Infrastructure.Reader; namespace TagCloudGeneratorTests { public class LineTextReaderTests { - private LineTextReader reader; - private string tempFilePath; - - [SetUp] - public void Setup() - { - reader = new LineTextReader(); - tempFilePath = Path.GetTempFileName(); - } + private LineTextReader reader = new LineTextReader(); + private string tempFilePath = Path.GetTempFileName(); [TearDown] public void TearDown() @@ -30,13 +15,8 @@ public void TearDown() File.Delete(tempFilePath); } - private void WriteToTempFile(string content) - { - File.WriteAllText(tempFilePath, content); - } - [Test] - public void TryRead_ExistingFile_ReturnsLines() + public void TryRead_ExistingFile_ReturnsLines_Test() { var expectedLines = new[] { "line1", "line2", "line3" }; WriteToTempFile(string.Join(Environment.NewLine, expectedLines)); @@ -46,7 +26,7 @@ public void TryRead_ExistingFile_ReturnsLines() } [Test] - public void TryRead_EmptyFile_ReturnsEmpty() + public void TryRead_EmptyFile_ReturnsEmpty_Test() { WriteToTempFile(""); var result = reader.TryRead(tempFilePath); @@ -54,7 +34,7 @@ public void TryRead_EmptyFile_ReturnsEmpty() } [Test] - public void TryRead_FileWithWhitespaceLines_ReturnsAllLines() + public void TryRead_FileWithWhitespaceLines_ReturnsAllLines_Test() { WriteToTempFile("line1\n\nline2\n \nline3"); var result = reader.TryRead(tempFilePath).ToList(); @@ -68,7 +48,7 @@ public void TryRead_FileWithWhitespaceLines_ReturnsAllLines() } [Test] - public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines() + public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines_Test() { WriteToTempFile("line1\r\nline2\r\nline3"); var result = reader.TryRead(tempFilePath).ToList(); @@ -77,7 +57,7 @@ public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines() } [Test] - public void TryRead_NonExistentFile_ReturnsNull() + public void TryRead_NonExistentFile_ReturnsNull_Test() { var nonExistentPath = "C:\\nonexistent\\file.txt"; var result = reader.TryRead(nonExistentPath); @@ -85,7 +65,7 @@ public void TryRead_NonExistentFile_ReturnsNull() } [Test] - public void TryRead_FileWithOneLine_ReturnsOneLine() + public void TryRead_FileWithOneLine_ReturnsOneLine_Test() { WriteToTempFile("single line"); var result = reader.TryRead(tempFilePath).ToList(); @@ -95,7 +75,7 @@ public void TryRead_FileWithOneLine_ReturnsOneLine() } [Test] - public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines() + public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines_Test() { WriteToTempFile("line1\nline2\n"); var result = reader.TryRead(tempFilePath).ToList(); @@ -104,17 +84,9 @@ public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines() Assert.That(result, Is.EqualTo(new[] { "line1", "line2" })); } - [Test] - public void TryRead_LargeFile_ReturnsAllLines() + private void WriteToTempFile(string content) { - var lines = Enumerable.Range(1, 1000).Select(i => $"Line {i}"); - WriteToTempFile(string.Join(Environment.NewLine, lines)); - - var result = reader.TryRead(tempFilePath).ToList(); - - Assert.That(result.Count, Is.EqualTo(1000)); - Assert.That(result[0], Is.EqualTo("line 1")); - Assert.That(result[999], Is.EqualTo("line 1000")); + File.WriteAllText(tempFilePath, content); } } } diff --git a/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs index b35f4353..b9fe20df 100644 --- a/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs +++ b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs @@ -1,67 +1,56 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using NUnit.Framework; +using NUnit.Framework; using TagCloudGenerator.Infrastructure.Calculators; namespace TagCloudGeneratorTests { public class LinearFontSizeCalculatorTests { - private LinearFontSizeCalculator calculator; - - [SetUp] - public void Setup() - { - calculator = new LinearFontSizeCalculator(); - } + private LinearFontSizeCalculator calculator = new LinearFontSizeCalculator(); [Test] - public void Calculate_MinFrequency_ReturnsMinFontSize() + public void Calculate_MinFrequency_ReturnsMinFontSize_Test() { var result = calculator.Calculate(1, 1, 10, 12f, 72f); Assert.That(result, Is.EqualTo(12f)); } [Test] - public void Calculate_MaxFrequency_ReturnsMaxFontSize() + public void Calculate_MaxFrequency_ReturnsMaxFontSize_Test() { var result = calculator.Calculate(10, 1, 10, 12f, 72f); Assert.That(result, Is.EqualTo(72f)); } [Test] - public void Calculate_MiddleFrequency_ReturnsProportionalSize() + public void Calculate_MiddleFrequency_ReturnsProportionalSize_Test() { var result = calculator.Calculate(5, 1, 9, 10f, 50f); Assert.That(result, Is.EqualTo(30f).Within(0.001f)); } [Test] - public void Calculate_SameMinMaxFrequency_ReturnsAverage() + public void Calculate_SameMinMaxFrequency_ReturnsAverage_Test() { var result = calculator.Calculate(5, 5, 5, 12f, 72f); Assert.That(result, Is.EqualTo(42f)); } [Test] - public void Calculate_FrequencyBelowMin_ReturnsLessThanMinFontSize() + public void Calculate_FrequencyBelowMin_ReturnsLessThanMinFontSize_Test() { var result = calculator.Calculate(0, 1, 10, 12f, 72f); Assert.That(result, Is.EqualTo(5.333f).Within(0.001f)); } [Test] - public void Calculate_FrequencyAboveMax_ReturnsMoreThanMaxFontSize() + public void Calculate_FrequencyAboveMax_ReturnsMoreThanMaxFontSize_Test() { var result = calculator.Calculate(15, 1, 10, 12f, 72f); Assert.That(result, Is.EqualTo(105.333f).Within(0.001f)); } [Test] - public void Calculate_ZeroRangeFontSizes_ReturnsMinFontSize() + public void Calculate_ZeroRangeFontSizes_ReturnsMinFontSize_Test() { var result = calculator.Calculate(5, 1, 10, 20f, 20f); Assert.That(result, Is.EqualTo(20f)); @@ -73,7 +62,7 @@ public void Calculate_ZeroRangeFontSizes_ReturnsMinFontSize() [TestCase(5, 30f)] [TestCase(7, 40f)] [TestCase(9, 50f)] - public void Calculate_MultipleTestCases_ReturnsCorrectValues(int frequency, float expected) + public void Calculate_MultipleTestCases_ReturnsCorrectValues_Test(int frequency, float expected) { var result = calculator.Calculate(frequency, 1, 9, 10f, 50f); Assert.That(result, Is.EqualTo(expected).Within(0.001f)); diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index fd019777..5197849e 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -1,32 +1,24 @@ -using TagCloudGenerator.Infrastructure.Analyzers; -using TagCloudGenerator.Infrastructure.Calculators; -using NUnit.Framework; -using System.Linq; +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Analyzers; namespace TagCloudGeneratorTests { public class WordsFrequencyAnalyzerTests { - private WordsFrequencyAnalyzer frequencyAnalyzer; - - [SetUp] - public void Setup() - { - frequencyAnalyzer = new WordsFrequencyAnalyzer(); - } + private WordsFrequencyAnalyzer frequencyAnalyzer = new WordsFrequencyAnalyzer(); [Test] - public void Analyze_EmptyInput_ReturnsEmpty() + public void Analyze_EmptyInput_ReturnsEmpty_Test() { - var words = Enumerable.Empty(); + var words = new List(); var result = frequencyAnalyzer.Analyze(words); Assert.That(result, Is.Empty); } [Test] - public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne() + public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() { - var words = new[] { "hello" }; + var words = new List { "hello" }; var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(1)); @@ -35,9 +27,9 @@ public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne() } [Test] - public void Analyze_MultipleWords_ReturnsCorrectFrequencies() + public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() { - var words = new[] { "hello", "world", "hello", "test", "world", "hello" }; + var words = new List { "hello", "world", "hello", "test", "world", "hello" }; var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); @@ -50,9 +42,9 @@ public void Analyze_MultipleWords_ReturnsCorrectFrequencies() } [Test] - public void Analyze_AllWordsEqualFrequency_ReturnsCorrectWeight() + public void Analyze_AllWordsEqualFrequency_ReturnsCorrectWeight_Test() { - var words = new[] { "a", "b", "c", "d" }; + var words = new List { "a", "b", "c", "d" }; var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(4)); @@ -63,9 +55,9 @@ public void Analyze_AllWordsEqualFrequency_ReturnsCorrectWeight() } [Test] - public void Analyze_DifferentFrequencies_ReturnsCorrectWeights() + public void Analyze_DifferentFrequencies_ReturnsCorrectWeights_Test() { - var words = new[] { "a", "a", "a", "b", "b", "c" }; + var words = new List { "a", "a", "a", "b", "b", "c" }; var result = frequencyAnalyzer.Analyze(words).ToList(); var itemA = result.First(i => i.Word == "a"); @@ -82,9 +74,9 @@ public void Analyze_DifferentFrequencies_ReturnsCorrectWeights() } [Test] - public void Analyze_CaseSensitive_ReturnsSeparateItems() + public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() { - var words = new[] { "Hello", "hello", "HELLO" }; + var words = new List { "Hello", "hello", "HELLO" }; var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); } From d79298ab689c9eabbaf9c35418bca14f3b4f566a Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Thu, 18 Dec 2025 02:15:35 +0500 Subject: [PATCH 3/8] Added new client and docx support --- .../ConsoleClient.cs | 26 +- .../Options.cs | 14 +- TagCloudConsoleClient/Program.cs | 25 + .../TagCloudConsoleClient.csproj | 14 + .../Algorithms/BasicTagCloudAlgorithm.cs | 5 + TagCloudGenerator/Clients/UiClient.cs | 12 - .../Core/Interfaces/IAnalyzer.cs | 2 +- .../Core/Interfaces/IFormatReader.cs | 14 + .../Core/Interfaces/INormalizer.cs | 13 + TagCloudGenerator/Core/Interfaces/IReader.cs | 1 + .../Core/Interfaces/IRenderer.cs | 5 +- .../Core/Interfaces/ITagCloudAlgorithm.cs | 3 + .../Core/Interfaces/ITagCloudGenerator.cs | 5 +- .../Core/Models/CanvasSettings.cs | 43 +- TagCloudGenerator/Core/Models/CloudItem.cs | 14 +- TagCloudGenerator/Core/Models/TextSettings.cs | 20 +- .../Core/Services/CloudGenerator.cs | 51 +- TagCloudGenerator/DI/TagCloudModule.cs | 16 +- .../Analyzers/WordsFrequencyAnalyzer.cs | 32 +- .../Filters/BoringWordsFilter.cs | 22 +- .../Filters/ToLowerCaseFilter.cs | 14 - .../Normalizers/LowerCaseNormalizer.cs | 14 + .../Infrastructure/Readers/CompositeReader.cs | 28 + .../Infrastructure/Readers/DocxReader.cs | 33 ++ .../{LineTextReader.cs => TxtReader.cs} | 9 +- .../Infrastructure/Renderers/PngRenderer.cs | 17 +- TagCloudGenerator/Program.cs | 1 - TagCloudGenerator/TagCloudGenerator.csproj | 5 + TagCloudGeneratorTests/CloudGeneratorTests.cs | 139 +++-- .../CompositeReaderTests.cs | 73 +++ TagCloudGeneratorTests/DocxReaderTests.cs | 99 ++++ ...neTextReaderTests.cs => TxtReaderTests.cs} | 6 +- .../WordsFrequencyAnalyzerTests.cs | 32 -- TagCloudUIClient/MainForm.Designer.cs | 479 ++++++++++++++++++ TagCloudUIClient/MainForm.cs | 30 ++ TagCloudUIClient/MainForm.resx | 120 +++++ TagCloudUIClient/Program.cs | 29 ++ TagCloudUIClient/TagCloudUIClient.csproj | 15 + TagCloudUIClient/WinFormsClient.cs | 27 + 39 files changed, 1257 insertions(+), 250 deletions(-) rename {TagCloudGenerator/Clients => TagCloudConsoleClient}/ConsoleClient.cs (72%) rename {TagCloudGenerator/Clients => TagCloudConsoleClient}/Options.cs (87%) create mode 100644 TagCloudConsoleClient/Program.cs create mode 100644 TagCloudConsoleClient/TagCloudConsoleClient.csproj delete mode 100644 TagCloudGenerator/Clients/UiClient.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFormatReader.cs create mode 100644 TagCloudGenerator/Core/Interfaces/INormalizer.cs delete mode 100644 TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs create mode 100644 TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/DocxReader.cs rename TagCloudGenerator/Infrastructure/Readers/{LineTextReader.cs => TxtReader.cs} (60%) create mode 100644 TagCloudGeneratorTests/CompositeReaderTests.cs create mode 100644 TagCloudGeneratorTests/DocxReaderTests.cs rename TagCloudGeneratorTests/{LineTextReaderTests.cs => TxtReaderTests.cs} (95%) create mode 100644 TagCloudUIClient/MainForm.Designer.cs create mode 100644 TagCloudUIClient/MainForm.cs create mode 100644 TagCloudUIClient/MainForm.resx create mode 100644 TagCloudUIClient/Program.cs create mode 100644 TagCloudUIClient/TagCloudUIClient.csproj create mode 100644 TagCloudUIClient/WinFormsClient.cs diff --git a/TagCloudGenerator/Clients/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs similarity index 72% rename from TagCloudGenerator/Clients/ConsoleClient.cs rename to TagCloudConsoleClient/ConsoleClient.cs index 964ee029..81e39111 100644 --- a/TagCloudGenerator/Clients/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -1,17 +1,29 @@ using CommandLine; +using System; +using System.Collections.Generic; using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; +using System.Text; +using System.Threading.Tasks; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; -namespace TagCloudGenerator.Clients +namespace TagCloudConsoleClient { public class ConsoleClient : IClient { private readonly ITagCloudGenerator _generator; + private readonly IEnumerable _filters; + private readonly IReader _reader; + private readonly INormalizer _normalizer; - public ConsoleClient(ITagCloudGenerator generator) + public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReader reader, INormalizer normalizer) { _generator = generator; + _filters = filters; + _reader = reader; + _normalizer = normalizer; } public void Run(string[] args) @@ -32,7 +44,6 @@ private void RunWithOptions(Options opts) var canvasSettings = new CanvasSettings() .SetSize(opts.Width, opts.Height) .SetBackgroundColor(TryParseColor(opts.BackgroundColor)) - .WithCenterCloud() .WithShowRectangles() .SetPadding(opts.Padding); @@ -50,7 +61,10 @@ private void RunWithOptions(Options opts) try { - _generator.Generate(inputFile, outputFile, canvasSettings, textSettings); + var words = _normalizer.Normalize(_reader.TryRead(inputFile)); + var image = _generator.Generate(words, canvasSettings, textSettings, _filters); + + if(image != null) image.Save(outputFile, ImageFormat.Png); Console.WriteLine("Tag cloud generation completed successfully!"); } catch (Exception ex) @@ -59,7 +73,7 @@ private void RunWithOptions(Options opts) } } - private static Color? TryParseColor(string? colorStr) + private static Color? TryParseColor(string colorStr) { if (string.IsNullOrWhiteSpace(colorStr)) return null; @@ -79,4 +93,4 @@ private void RunWithOptions(Options opts) } } } -} \ No newline at end of file +} diff --git a/TagCloudGenerator/Clients/Options.cs b/TagCloudConsoleClient/Options.cs similarity index 87% rename from TagCloudGenerator/Clients/Options.cs rename to TagCloudConsoleClient/Options.cs index 30aac4f0..df1790fa 100644 --- a/TagCloudGenerator/Clients/Options.cs +++ b/TagCloudConsoleClient/Options.cs @@ -1,6 +1,12 @@ -using CommandLine; - -namespace TagCloudGenerator.Clients +using CommandLine.Text; +using CommandLine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudConsoleClient { public class Options { @@ -38,4 +44,4 @@ public class Options [Option("maxsize", HelpText = "Maximum font size.")] public float? MaxFontSize { get; set; } } -} \ No newline at end of file +} diff --git a/TagCloudConsoleClient/Program.cs b/TagCloudConsoleClient/Program.cs new file mode 100644 index 00000000..4944c8f9 --- /dev/null +++ b/TagCloudConsoleClient/Program.cs @@ -0,0 +1,25 @@ +using Autofac; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.DI; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudConsoleClient +{ + public class Program + { + public static void Main(string[] args) + { + var builder = new ContainerBuilder(); + + builder.RegisterModule(); + builder.RegisterType().As(); + builder.RegisterType().As(); + + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var client = scope.Resolve(); + client.Run(args); + } + } +} \ No newline at end of file diff --git a/TagCloudConsoleClient/TagCloudConsoleClient.csproj b/TagCloudConsoleClient/TagCloudConsoleClient.csproj new file mode 100644 index 00000000..d1c033ae --- /dev/null +++ b/TagCloudConsoleClient/TagCloudConsoleClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs index 75bad782..f856af9d 100644 --- a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -91,5 +91,10 @@ public BasicTagCloudAlgorithm WithCenterAt(Point point) return this; } + public BasicTagCloudAlgorithm Reset() + { + rectangles.Clear(); + return this; + } } } diff --git a/TagCloudGenerator/Clients/UiClient.cs b/TagCloudGenerator/Clients/UiClient.cs deleted file mode 100644 index 92e5eafd..00000000 --- a/TagCloudGenerator/Clients/UiClient.cs +++ /dev/null @@ -1,12 +0,0 @@ -using TagCloudGenerator.Core.Interfaces; - -namespace TagCloudGenerator.Clients -{ - public class UiClient : IClient - { - public void Run(string[] args) - { - throw new NotImplementedException(); - } - } -} diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs index de6b87e0..332b7136 100644 --- a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -4,6 +4,6 @@ namespace TagCloudGenerator.Core.Interfaces { public interface IAnalyzer { - List Analyze(List words); + List<(string Word, int Frequency)> Analyze(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs new file mode 100644 index 00000000..5be6f11d --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFormatReader + { + bool CanRead(string filePath); + List TryRead(string filePath); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/INormalizer.cs b/TagCloudGenerator/Core/Interfaces/INormalizer.cs new file mode 100644 index 00000000..ef2224f4 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface INormalizer + { + public List Normalize(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IReader.cs b/TagCloudGenerator/Core/Interfaces/IReader.cs index 7890629d..991c7d17 100644 --- a/TagCloudGenerator/Core/Interfaces/IReader.cs +++ b/TagCloudGenerator/Core/Interfaces/IReader.cs @@ -6,5 +6,6 @@ namespace TagCloudGenerator.Core.Interfaces public interface IReader { List TryRead(string filePath); + bool CanRead(string filePath); } } diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs index 452b9884..990f5dcd 100644 --- a/TagCloudGenerator/Core/Interfaces/IRenderer.cs +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -1,9 +1,10 @@ -using TagCloudGenerator.Core.Models; +using System.Drawing; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Core.Interfaces { public interface IRenderer { - void Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings, string outputFile); + public Bitmap Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs index 3f7a7ad1..50b2cdd9 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -1,9 +1,12 @@ using System.Drawing; +using TagCloudGenerator.Algorithms; namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudAlgorithm { Rectangle PutNextRectangle(Size rectangleSize); + + public BasicTagCloudAlgorithm Reset(); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs index 25feb60a..5bb4c556 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -1,9 +1,10 @@ -using TagCloudGenerator.Core.Models; +using System.Drawing; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudGenerator { - public void Generate(string inputFile, string outputFile, CanvasSettings canvasSettings, TextSettings textSettings); + public Bitmap? Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters); } } diff --git a/TagCloudGenerator/Core/Models/CanvasSettings.cs b/TagCloudGenerator/Core/Models/CanvasSettings.cs index eab79127..2f4beafa 100644 --- a/TagCloudGenerator/Core/Models/CanvasSettings.cs +++ b/TagCloudGenerator/Core/Models/CanvasSettings.cs @@ -4,34 +4,38 @@ namespace TagCloudGenerator.Core.Models { public class CanvasSettings { - public Color BackgroundColor { get; set; } = Color.White; - public Size CanvasSize { get; set; } = new Size(1000, 1000); - public bool CenterCloud { get; set; } = true; - public bool ShowRectangles { get; set; } = true; - public int Padding { get; set; } = 50; + public Color BackgroundColor { get; private set; } = Color.White; + public Size CanvasSize { get; private set; } = new Size(1000, 1000); + public bool ShowRectangles { get; private set; } = true; + public int EdgePadding { get; private set; } = 50; public CanvasSettings SetWidth(int? width) { - if (width.HasValue && width > 0) - CanvasSize = new Size(width.Value, CanvasSize.Height); + var size = CanvasSize; + + size.Width = width is > 0 ? width.Value : CanvasSize.Width; + CanvasSize = size; + return this; } public CanvasSettings SetHeight(int? height) { - if (height.HasValue && height > 0) - CanvasSize = new Size(CanvasSize.Width, height.Value); + var size = CanvasSize; + + size.Height = height is > 0 ? height.Value : CanvasSize.Height; + CanvasSize = size; + return this; } public CanvasSettings SetSize(int? width, int? height) { - if (width.HasValue && height.HasValue && width > 0 && height > 0) - CanvasSize = new Size(width.Value, height.Value); - else if (width.HasValue && width > 0) - CanvasSize = new Size(width.Value, CanvasSize.Height); - else if (height.HasValue && height > 0) - CanvasSize = new Size(CanvasSize.Width, height.Value); + var size = CanvasSize; + + size.Width = width is > 0 ? width.Value : CanvasSize.Width; + size.Height = height is > 0 ? height.Value : CanvasSize.Height; + CanvasSize = size; return this; } @@ -43,12 +47,6 @@ public CanvasSettings SetBackgroundColor(Color? color) return this; } - public CanvasSettings WithCenterCloud(bool value = true) - { - CenterCloud = value; - return this; - } - public CanvasSettings WithShowRectangles(bool value = true) { ShowRectangles = value; @@ -57,8 +55,7 @@ public CanvasSettings WithShowRectangles(bool value = true) public CanvasSettings SetPadding(int? padding) { - if (padding.HasValue && padding >= 0) - Padding = (int)padding; + EdgePadding = padding is >= 0 ? (int)padding.Value : EdgePadding; return this; } } diff --git a/TagCloudGenerator/Core/Models/CloudItem.cs b/TagCloudGenerator/Core/Models/CloudItem.cs index 8dc368b7..c6a00eea 100644 --- a/TagCloudGenerator/Core/Models/CloudItem.cs +++ b/TagCloudGenerator/Core/Models/CloudItem.cs @@ -7,17 +7,16 @@ public class CloudItem public string Word { get; } public Rectangle Rectangle { get; } public float FontSize { get; } - public Color? Color { get; } + public Color? TextColor { get; } public string FontFamily { get; } public FontStyle FontStyle { get; } public int Frequency { get; } - public double Weight { get; } public CloudItem( string word, Rectangle rectangle, float fontSize, - Color? color = null, + Color? textColor = null, string fontFamily = "Arial", FontStyle fontStyle = FontStyle.Regular, int frequency = 1, @@ -26,26 +25,25 @@ public CloudItem( Word = word ?? throw new ArgumentNullException(nameof(word)); Rectangle = rectangle; FontSize = fontSize; - Color = color; + TextColor = textColor; FontFamily = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily)); FontStyle = fontStyle; Frequency = frequency; - Weight = weight; } public CloudItem WithRectangle(Rectangle newRectangle) { - return new CloudItem(Word, newRectangle, FontSize, Color, FontFamily, FontStyle, Frequency, Weight); + return new CloudItem(Word, newRectangle, FontSize, TextColor, FontFamily, FontStyle, Frequency); } public CloudItem WithFontSize(float newFontSize) { - return new CloudItem(Word, Rectangle, newFontSize, Color, FontFamily, FontStyle, Frequency, Weight); + return new CloudItem(Word, Rectangle, newFontSize, TextColor, FontFamily, FontStyle, Frequency); } public CloudItem WithColor(Color newColor) { - return new CloudItem(Word, Rectangle, FontSize, newColor, FontFamily, FontStyle, Frequency, Weight); + return new CloudItem(Word, Rectangle, FontSize, newColor, FontFamily, FontStyle, Frequency); } } } diff --git a/TagCloudGenerator/Core/Models/TextSettings.cs b/TagCloudGenerator/Core/Models/TextSettings.cs index 53a35b8e..a7ca6145 100644 --- a/TagCloudGenerator/Core/Models/TextSettings.cs +++ b/TagCloudGenerator/Core/Models/TextSettings.cs @@ -4,29 +4,26 @@ namespace TagCloudGenerator.Core.Models { public class TextSettings { - public string FontFamily { get; set; } = "Arial"; - public float MinFontSize { get; set; } = 12f; - public float MaxFontSize { get; set; } = 72f; - public Color TextColor { get; set; } = Color.Black; + public string FontFamily { get; private set; } = "Arial"; + public float MinFontSize { get; private set; } = 12f; + public float MaxFontSize { get; private set; } = 72f; + public Color TextColor { get; private set; } = Color.Black; public TextSettings SetFontFamily(string? font) { - if (!string.IsNullOrWhiteSpace(font)) - FontFamily = font; + if (!string.IsNullOrWhiteSpace(font)) FontFamily = font; return this; } public TextSettings SetMinFontSize(float? size) { - if (size.HasValue && size.Value >= 0) - MinFontSize = size.Value; + MinFontSize = size is >= 0 ? size.Value : MinFontSize; return this; } public TextSettings SetMaxFontSize(float? size) { - if (size.HasValue && size.Value >= 0) - MaxFontSize = size.Value; + MaxFontSize = size is >= 0 ? size.Value : MaxFontSize; return this; } @@ -39,8 +36,7 @@ public TextSettings SetFontSizeRange(float? minSize, float? maxSize) public TextSettings SetTextColor(Color? color) { - if (color.HasValue) - TextColor = color.Value; + if (color.HasValue) TextColor = color.Value; return this; } } diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index 9e693756..448c6036 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -1,14 +1,14 @@ using System.Drawing; +using TagCloudGenerator.Algorithms; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudGenerator.Core.Services { public class CloudGenerator : ITagCloudGenerator { private readonly ITagCloudAlgorithm _algorithm; - private readonly IReader _reader; - private readonly IEnumerable _filters; private readonly IAnalyzer _analyzer; private readonly IRenderer _renderer; private readonly IFontSizeCalculator _fontSizeCalculator; @@ -16,16 +16,12 @@ public class CloudGenerator : ITagCloudGenerator private readonly Point _center; public CloudGenerator(ITagCloudAlgorithm algorithm, - IReader reader, - IEnumerable filters, IAnalyzer analyzer, IRenderer renderer, IFontSizeCalculator fontSizeCalculator, ITextMeasurer textMeasurer) { - _algorithm = algorithm; - _reader = reader; - _filters = filters; + this._algorithm = algorithm; _analyzer = analyzer; _renderer = renderer; _fontSizeCalculator = fontSizeCalculator; @@ -33,37 +29,31 @@ public CloudGenerator(ITagCloudAlgorithm algorithm, _center = new Point(0, 0); } - public void Generate(string inputFile, string outputFile, CanvasSettings canvasSettings, TextSettings textSettings) + public Bitmap? Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) { - var words = _reader.TryRead(inputFile).ToList(); - - if (words == null || words.Count == 0) - return; + words = ApplyFilters(words, filters); + if(words.Count == 0) return null; - words = ApplyFilters(words); - if(words.Count == 0) - return; + var wordsWithFreq = _analyzer.Analyze(words); - var cloudItems = _analyzer.Analyze(words).ToList(); + var initializedItems = InitializeCloudItems(wordsWithFreq, textSettings).ToList(); - var preparedItems = PrepareCloudItems(cloudItems, textSettings).ToList(); + _algorithm.Reset(); - var arrangedItems = ArrangeCloudItems(preparedItems).ToList(); - - _renderer.Render(arrangedItems, canvasSettings, textSettings, outputFile); + return _renderer.Render(initializedItems, canvasSettings, textSettings); } - private List ApplyFilters(List words) + private List ApplyFilters(List words, IEnumerable filters) { var filteredWords = words; - foreach (var filter in _filters) + foreach (var filter in filters) { filteredWords = filter.Filter(filteredWords); } return filteredWords; } - private IEnumerable PrepareCloudItems(IEnumerable items, TextSettings settings) + private IEnumerable InitializeCloudItems(IEnumerable<(string Word, int Frequency)> items, TextSettings settings) { var itemsList = new List(); @@ -84,24 +74,19 @@ private IEnumerable PrepareCloudItems(IEnumerable items, T fontSize, settings.FontFamily); + var itemRectangle = _algorithm.PutNextRectangle(textSize); + itemsList.Add( new CloudItem( word: item.Word, - rectangle: new Rectangle(Point.Empty, textSize), + rectangle: itemRectangle, fontSize: fontSize, - color: settings.TextColor, + textColor: settings.TextColor, fontFamily: settings.FontFamily, - frequency: item.Frequency, - weight: item.Weight + frequency: item.Frequency )); } return itemsList; } - - private List ArrangeCloudItems(List items) - { - return items.Select(item => item.WithRectangle(_algorithm.PutNextRectangle(item.Rectangle.Size))) - .ToList(); - } } } diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs index aad1b83f..67553bb7 100644 --- a/TagCloudGenerator/DI/TagCloudModule.cs +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -1,13 +1,13 @@ using Autofac; using TagCloudGenerator.Algorithms; -using TagCloudGenerator.Clients; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Services; using TagCloudGenerator.Infrastructure.Analyzers; using TagCloudGenerator.Infrastructure.Calculators; using TagCloudGenerator.Infrastructure.Filters; using TagCloudGenerator.Infrastructure.Measurers; -using TagCloudGenerator.Infrastructure.Reader; +using TagCloudGenerator.Infrastructure.Normalizers; +using TagCloudGenerator.Infrastructure.Readers; using TagCloudGenerator.Infrastructure.Renderers; namespace TagCloudGenerator.DI @@ -16,10 +16,14 @@ public class TagCloudModule : Autofac.Module { protected override void Load(ContainerBuilder builder) { - builder.RegisterType().As(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As(); - builder.RegisterType().As(); + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType().As(); builder.RegisterType().As(); @@ -32,8 +36,6 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType() .As(); - builder.RegisterType().As(); - builder.RegisterType().As(); builder.RegisterType().As(); diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index e106f660..90e5cf35 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -6,28 +6,26 @@ namespace TagCloudGenerator.Infrastructure.Analyzers { public class WordsFrequencyAnalyzer : IAnalyzer { - public List Analyze(List words) + public List<(string Word, int Frequency)> Analyze(List words) { - if (words == null || words.Count == 0) - return new List(); + var wordFreqDictionary = new Dictionary(); - var wordGroups = words - .GroupBy(word => word) - .Select(group => new { Word = group.Key, Frequency = group.Count() }) - .OrderByDescending(x => x.Frequency); + if (words == null || words.Count == 0) + return new List<(string word, int freq)>(); - if (wordGroups.Count() == 0) - return new List(); + foreach (string word in words) + { + if (!wordFreqDictionary.ContainsKey(word)) wordFreqDictionary.Add(word, 1); + else wordFreqDictionary[word]++; + } - var maxFreq = wordGroups.Max(g => g.Frequency); + var result = wordFreqDictionary + .OrderByDescending(pair => pair.Value) + .ThenBy(pair => pair.Key) + .Select(pair => (pair.Key, pair.Value)) + .ToList(); - return wordGroups.Select(group => new CloudItem( - word: group.Word, - rectangle: Rectangle.Empty, - fontSize: 0, - frequency: group.Frequency, - weight: (double)group.Frequency / maxFreq - )).ToList(); + return result; } } } diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs index ae05d2b5..56589740 100644 --- a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -4,16 +4,30 @@ namespace TagCloudGenerator.Infrastructure.Filters { public class BoringWordsFilter : IFilter { - private readonly string[] _boringWords = ["in", "it", "a", "as", "for", "of", "on"]; + private string[] boringWords = ["in", "it", "a", "as", "for", "of", "on"]; + + public BoringWordsFilter() { } + + public BoringWordsFilter(string[] words) + { + boringWords = words; + } + + public BoringWordsFilter(List wordsList) + { + boringWords = wordsList.ToArray(); + } + public List Filter(List words) { - if (words == null || words.Count() == 0) return new List(); - return words.Where(w => ShouldInclude(w)).ToList(); + if (words == null || words.Count == 0) return new List(); + + return words.Where(w => !boringWords.Contains(w)).ToList(); } public bool ShouldInclude(string word) { - return !_boringWords.Contains(word); + return !boringWords.Contains(word); } } } diff --git a/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs b/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs deleted file mode 100644 index 43d48a6e..00000000 --- a/TagCloudGenerator/Infrastructure/Filters/ToLowerCaseFilter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using TagCloudGenerator.Core.Interfaces; - -namespace TagCloudGenerator.Infrastructure.Filters -{ - public class ToLowerCaseFilter : IFilter - { - public List Filter(List words) - { - if (words == null || words.Count() == 0) return new List(); - - return words.Select(w => w.ToLower()).ToList(); - } - } -} diff --git a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs new file mode 100644 index 00000000..83ef709b --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs @@ -0,0 +1,14 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Normalizers +{ + public class LowerCaseNormalizer : INormalizer + { + public List Normalize(List words) + { + if (words == null || words.Count == 0) return new List(); + + return words.Select(w => w.ToLower()).ToList(); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs b/TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs new file mode 100644 index 00000000..f2639749 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs @@ -0,0 +1,28 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class CompositeReader : IReader + { + private readonly IEnumerable _readers; + + public CompositeReader(IEnumerable readers) + { + _readers = readers; + } + + public bool CanRead(string filePath) + { + return _readers.Any(r => r.CanRead(filePath)); + } + + public List TryRead(string filePath) + { + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + if (reader == null) + throw new NotSupportedException("Формат не поддерживается"); + + return reader.TryRead(filePath); + } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs new file mode 100644 index 00000000..81e95317 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -0,0 +1,33 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class DocxReader : IFormatReader + { + public bool CanRead(string filePath) + { + return Path.GetExtension(filePath).Equals(".docx", StringComparison.OrdinalIgnoreCase); + } + + public List TryRead(string filePath) + { + try + { + using var doc = WordprocessingDocument.Open(filePath, false); + + return doc.MainDocumentPart.Document.Body + .Elements() + .Select(p => p.InnerText) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToList(); + } + catch (Exception e) + { + Console.WriteLine($"DOCX read error: {e.Message}"); + return new List(); + } + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs similarity index 60% rename from TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs rename to TagCloudGenerator/Infrastructure/Readers/TxtReader.cs index b1999e04..ede68db2 100644 --- a/TagCloudGenerator/Infrastructure/Readers/LineTextReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs @@ -1,9 +1,14 @@ using TagCloudGenerator.Core.Interfaces; -namespace TagCloudGenerator.Infrastructure.Reader +namespace TagCloudGenerator.Infrastructure.Readers { - public class LineTextReader : IReader + public class TxtReader : IFormatReader { + public bool CanRead(string filePath) + { + return Path.GetExtension(filePath).Equals(".txt", StringComparison.OrdinalIgnoreCase); + } + public List TryRead(string filePath) { try diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index 27833fcb..e44fddfd 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -7,16 +7,12 @@ namespace TagCloudGenerator.Infrastructure.Renderers { public class PngRenderer : IRenderer { - public void Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings, string outputFile) + public Bitmap Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) { var itemsList = items.ToList(); - if (items.Count() == 0) - { - Console.WriteLine("No elements to render"); - return; - } - using var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + using var graphics = Graphics.FromImage(bitmap); ConfigureGraphics(graphics); @@ -33,16 +29,13 @@ public void Render(IEnumerable items, CanvasSettings canvasSettings, DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); } - bitmap.Save(outputFile, ImageFormat.Png); + return bitmap; } private (int offsetX, int offsetY) CalculateOffset( List items, CanvasSettings settings) { - if (!settings.CenterCloud) - return (settings.Padding, settings.Padding); - var minX = items.Min(i => i.Rectangle.X); var minY = items.Min(i => i.Rectangle.Y); var maxX = items.Max(i => i.Rectangle.Right); @@ -74,7 +67,7 @@ private void DrawCloudItem( item.Rectangle.Width, item.Rectangle.Height); - var color = item.Color ?? textSettings.TextColor; + var color = item.TextColor ?? textSettings.TextColor; using var font = new Font( item.FontFamily, diff --git a/TagCloudGenerator/Program.cs b/TagCloudGenerator/Program.cs index 146a99de..eb41ef57 100644 --- a/TagCloudGenerator/Program.cs +++ b/TagCloudGenerator/Program.cs @@ -1,6 +1,5 @@ using System; using Autofac; -using TagCloudGenerator.Clients; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.DI; diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj index e21897b5..4fc29757 100644 --- a/TagCloudGenerator/TagCloudGenerator.csproj +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -7,9 +7,14 @@ disable + + + + + diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index 3ddb96d1..f357a601 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -10,7 +10,6 @@ namespace TagCloudGeneratorTests public class CloudGeneratorTests { private Mock algorithmMock; - private Mock readerMock; private Mock filterMock; private Mock analyzerMock; private Mock rendererMock; @@ -22,7 +21,6 @@ public class CloudGeneratorTests public void Setup() { algorithmMock = new Mock(); - readerMock = new Mock(); filterMock = new Mock(); analyzerMock = new Mock(); rendererMock = new Mock(); @@ -31,8 +29,6 @@ public void Setup() cloudGenerator = new CloudGenerator( algorithmMock.Object, - readerMock.Object, - new[] { filterMock.Object }, analyzerMock.Object, rendererMock.Object, fontSizeCalculatorMock.Object, @@ -40,77 +36,109 @@ public void Setup() } [Test] - public void Generate_EmptyFile_DoesNothing_Test() + public void Generate_EmptyWords_ReturnsNull_AndDoesNotRender_Test() { - readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(new List()); + filterMock + .Setup(f => f.Filter(It.IsAny>())) + .Returns(new List()); - cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), new TextSettings()); + var result = cloudGenerator.Generate( + new List(), + new CanvasSettings(), + new TextSettings(), + new[] { filterMock.Object }); + + Assert.IsNull(result); rendererMock.Verify(r => r.Render( It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); + It.IsAny()), + Times.Never); } [Test] - public void Generate_AllWordsFilteredOut_DoesNothing_Test() + public void Generate_AllWordsFilteredOut_ReturnsNull_Test() { var words = new List { "in", "a", "for" }; - readerMock.Setup(r => r.TryRead(It.IsAny())).Returns(words); - filterMock.Setup(f => f.Filter(It.IsAny>())).Returns(new List()); - cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), new TextSettings()); + filterMock + .Setup(f => f.Filter(words)) + .Returns(new List()); + + var result = cloudGenerator.Generate( + words, + new CanvasSettings(), + new TextSettings(), + new[] { filterMock.Object }); + + Assert.IsNull(result); rendererMock.Verify(r => r.Render( It.IsAny>(), It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Never); + It.IsAny()), + Times.Never); } [Test] public void Generate_NormalFlow_CallsAllDependencies_Test() { - var words = new List { "hello", "world", "test" }; - var filteredWords = new List { "hello", "world", "test" }; - var cloudItems = new List - { - new CloudItem("hello", Rectangle.Empty, 0, frequency: 2), - new CloudItem("world", Rectangle.Empty, 0, frequency: 1) - }; - - readerMock.Setup(r => r.TryRead("input.txt")).Returns(words); - filterMock.Setup(f => f.Filter(words)).Returns(filteredWords); - analyzerMock.Setup(a => a.Analyze(filteredWords)).Returns(cloudItems); - - var textSettings = new TextSettings - { - FontFamily = "Arial", - MinFontSize = 12f, - MaxFontSize = 72f, - TextColor = Color.Black - }; - - fontSizeCalculatorMock.Setup(f => f.Calculate(2, 1, 2, 12f, 72f)).Returns(50f); - fontSizeCalculatorMock.Setup(f => f.Calculate(1, 1, 2, 12f, 72f)).Returns(20f); - - textMeasurerMock.Setup(t => t.Measure("hello", 50f, "Arial")).Returns(new Size(100, 30)); - textMeasurerMock.Setup(t => t.Measure("world", 20f, "Arial")).Returns(new Size(80, 25)); - - algorithmMock.Setup(a => a.PutNextRectangle(new Size(100, 30))).Returns(new Rectangle(0, 0, 100, 30)); - algorithmMock.Setup(a => a.PutNextRectangle(new Size(80, 25))).Returns(new Rectangle(100, 0, 80, 25)); - - cloudGenerator.Generate("input.txt", "output.png", new CanvasSettings(), textSettings); - - readerMock.Verify(r => r.TryRead("input.txt"), Times.Once); + var words = new List { "hello", "world", "hello" }; + var filteredWords = new List { "hello", "world", "hello" }; + + var analyzed = new List<(string Word, int Frequency)> { ("hello", 2), ("world", 1) }; + + filterMock + .Setup(f => f.Filter(words)) + .Returns(filteredWords); + + analyzerMock + .Setup(a => a.Analyze(filteredWords)) + .Returns(analyzed); + + var textSettings = new TextSettings() + .SetFontFamily("Arial") + .SetFontSizeRange(12, 72); + + fontSizeCalculatorMock + .Setup(f => f.Calculate(2, 1, 2, 12f, 72f)) + .Returns(50f); + + fontSizeCalculatorMock + .Setup(f => f.Calculate(1, 1, 2, 12f, 72f)) + .Returns(20f); + + textMeasurerMock + .Setup(t => t.Measure("hello", 50f, "Arial")) + .Returns(new Size(100, 30)); + + textMeasurerMock + .Setup(t => t.Measure("world", 20f, "Arial")) + .Returns(new Size(80, 25)); + + algorithmMock + .Setup(a => a.PutNextRectangle(It.IsAny())) + .Returns(new Rectangle(0, 0, 100, 30)); + + rendererMock + .Setup(r => r.Render( + It.IsAny>(), + It.IsAny(), + It.IsAny())) + .Returns(new Bitmap(1, 1)); + + var result = cloudGenerator.Generate( + words, + new CanvasSettings(), + textSettings, + new[] { filterMock.Object }); + + Assert.IsNotNull(result); + filterMock.Verify(f => f.Filter(words), Times.Once); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); - - fontSizeCalculatorMock.Verify(f => - f.Calculate(It.IsAny(), 1, 2, 12f, 72f), Times.Exactly(2)); - textMeasurerMock.Verify(t => - t.Measure(It.IsAny(), It.IsAny(), "Arial"), Times.Exactly(2)); + algorithmMock.Verify(a => a.Reset(), Times.Once); algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); rendererMock.Verify(r => r.Render( @@ -118,9 +146,10 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() It.IsAny(), It.Is(ts => ts.FontFamily == "Arial" && - ts.MinFontSize == 12f && - ts.MaxFontSize == 72f), - "output.png"), Times.Once); + Math.Abs(ts.MinFontSize - 12) < float.Epsilon && + Math.Abs(ts.MaxFontSize - 72) < float.Epsilon)), + Times.Once); } + } } diff --git a/TagCloudGeneratorTests/CompositeReaderTests.cs b/TagCloudGeneratorTests/CompositeReaderTests.cs new file mode 100644 index 00000000..738d3059 --- /dev/null +++ b/TagCloudGeneratorTests/CompositeReaderTests.cs @@ -0,0 +1,73 @@ +using Moq; +using NUnit.Framework; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class CompositeReaderTests + { + private Mock reader1Mock; + private Mock reader2Mock; + private CompositeReader compositeReader; + + [SetUp] + public void Setup() + { + reader1Mock = new Mock(); + reader2Mock = new Mock(); + + compositeReader = new CompositeReader( + new[] { reader1Mock.Object, reader2Mock.Object }); + } + + [Test] + public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() + { + reader1Mock.Setup(r => r.CanRead("file.docx")).Returns(false); + reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); + + var result = compositeReader.CanRead("file.docx"); + + Assert.That(result, Is.True); + } + + [Test] + public void CanRead_WhenNoReaderCanRead_ReturnsFalse() + { + reader1Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + + var result = compositeReader.CanRead("file.unknown"); + + Assert.That(result, Is.False); + } + + [Test] + public void TryRead_UsesFirstMatchingReader() + { + var expected = new List { "word1", "word2" }; + + reader1Mock.Setup(r => r.CanRead("file.docx")).Returns(false); + reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); + reader2Mock.Setup(r => r.TryRead("file.docx")).Returns(expected); + + var result = compositeReader.TryRead("file.docx"); + + Assert.That(result, Is.EqualTo(expected)); + + reader2Mock.Verify(r => r.TryRead("file.docx"), Times.Once); + reader1Mock.Verify(r => r.TryRead(It.IsAny()), Times.Never); + } + + [Test] + public void TryRead_WhenNoReaderFound_ThrowsException() + { + reader1Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + + Assert.Throws(() => + compositeReader.TryRead("file.xyz")); + } + } +} diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs new file mode 100644 index 00000000..f4dc74b8 --- /dev/null +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -0,0 +1,99 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class DocxReaderTests + { + private string tempFilePath; + private DocxReader reader; + + [SetUp] + public void Setup() + { + tempFilePath = Path.Combine( + Path.GetTempPath(), + Guid.NewGuid() + ".docx"); + + reader = new DocxReader(); + } + + [TearDown] + public void TearDown() + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + + [Test] + public void CanRead_DocxExtension_ReturnsTrue_Test() + { + Assert.That(reader.CanRead("file.docx"), Is.True); + Assert.That(reader.CanRead("file.txt"), Is.False); + } + + [Test] + public void TryRead_DocxWithParagraphs_ReturnsParagraphs_Test() + { + CreateDocx(new[] { "word1", "word2", "word3" }); + + var result = reader.TryRead(tempFilePath); + + Assert.That(result, Is.EqualTo(new[] { "word1", "word2", "word3" })); + } + + [Test] + public void TryRead_EmptyDocx_ReturnsEmptyList_Test() + { + CreateDocx(Array.Empty()); + + var result = reader.TryRead(tempFilePath); + + Assert.That(result, Is.Empty); + } + + [Test] + public void TryRead_DocxWithEmptyParagraphs_IgnoresThem_Test() + { + CreateDocx(new[] { "word1", "", " ", "word2" }); + + var result = reader.TryRead(tempFilePath); + + Assert.That(result, Is.EqualTo(new[] { "word1", "word2" })); + } + + [Test] + public void TryRead_NonExistentFile_ReturnsEmptyList_Test() + { + var result = reader.TryRead("nonexistent.docx"); + + Assert.That(result, Is.Empty); + } + + private void CreateDocx(IEnumerable lines) + { + using var doc = WordprocessingDocument.Create( + tempFilePath, + DocumentFormat.OpenXml.WordprocessingDocumentType.Document); + + var body = new Body(); + + foreach (var line in lines) + { + body.AppendChild( + new Paragraph( + new Run(new Text(line)))); + } + + doc.AddMainDocumentPart().Document = + new Document(body); + } + } +} diff --git a/TagCloudGeneratorTests/LineTextReaderTests.cs b/TagCloudGeneratorTests/TxtReaderTests.cs similarity index 95% rename from TagCloudGeneratorTests/LineTextReaderTests.cs rename to TagCloudGeneratorTests/TxtReaderTests.cs index f71f42a6..e9116c8e 100644 --- a/TagCloudGeneratorTests/LineTextReaderTests.cs +++ b/TagCloudGeneratorTests/TxtReaderTests.cs @@ -1,11 +1,11 @@ using NUnit.Framework; -using TagCloudGenerator.Infrastructure.Reader; +using TagCloudGenerator.Infrastructure.Readers; namespace TagCloudGeneratorTests { - public class LineTextReaderTests + public class TxtReaderTests { - private LineTextReader reader = new LineTextReader(); + private TxtReader reader = new TxtReader(); private string tempFilePath = Path.GetTempFileName(); [TearDown] diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index 5197849e..c3bf6f39 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -41,38 +41,6 @@ public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() Assert.That(result[2].Frequency, Is.EqualTo(1)); } - [Test] - public void Analyze_AllWordsEqualFrequency_ReturnsCorrectWeight_Test() - { - var words = new List { "a", "b", "c", "d" }; - var result = frequencyAnalyzer.Analyze(words).ToList(); - - Assert.That(result.Count, Is.EqualTo(4)); - foreach (var item in result) - { - Assert.That(item.Weight, Is.EqualTo(1.0).Within(0.001)); - } - } - - [Test] - public void Analyze_DifferentFrequencies_ReturnsCorrectWeights_Test() - { - var words = new List { "a", "a", "a", "b", "b", "c" }; - var result = frequencyAnalyzer.Analyze(words).ToList(); - - var itemA = result.First(i => i.Word == "a"); - var itemB = result.First(i => i.Word == "b"); - var itemC = result.First(i => i.Word == "c"); - - Assert.That(itemA.Frequency, Is.EqualTo(3)); - Assert.That(itemB.Frequency, Is.EqualTo(2)); - Assert.That(itemC.Frequency, Is.EqualTo(1)); - - Assert.That(itemA.Weight, Is.EqualTo(1.0).Within(0.001)); - Assert.That(itemB.Weight, Is.EqualTo(0.666).Within(0.001)); - Assert.That(itemC.Weight, Is.EqualTo(0.333).Within(0.001)); - } - [Test] public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() { diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs new file mode 100644 index 00000000..90154979 --- /dev/null +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -0,0 +1,479 @@ +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudUIClient +{ + public partial class MainForm : Form + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + private readonly ITagCloudGenerator _generator; + private readonly IReader _reader; + private readonly INormalizer _normalizer; + private string _lastOutputPath = string.Empty; + + private List wordsToRender; + + private Bitmap? _generatedBitmap; + + private Button btnChooseFile; + private Label lblFilePath; + private GroupBox groupBoxSettings; + private Label lblWidth; + private NumericUpDown numWidth; + private Label lblHeight; + private NumericUpDown numHeight; + private Label lblMinSize; + private NumericUpDown numMinSize; + private Label lblMaxSize; + private NumericUpDown numMaxSize; + private Button btnChooseFont; + private Label lblFont; + private Button btnTextColor; + private Button btnBgColor; + private CheckedListBox clbExcludedWords; + private Button btnGenerate; + private PictureBox picturePreview; + private Button btnSave; + + + public MainForm(ITagCloudGenerator generator, IReader reader, INormalizer normalizer) + { + _generator = generator; + _reader = reader; + _normalizer = normalizer; + InitializeComponent(); + } + + private void btnChooseFile_Click(object sender, EventArgs e) + { + using var dlg = new OpenFileDialog(); + dlg.Filter = "Text files (*.txt)|*.txt|All files (*.*)|*.*"; + if (dlg.ShowDialog() == DialogResult.OK) + { + lblFilePath.Text = dlg.FileName; + LoadExcludedWords(dlg.FileName); + } + } + + private void LoadExcludedWords(string path) + { + clbExcludedWords.Items.Clear(); + + var words = _reader.TryRead(path); + wordsToRender = _normalizer.Normalize(words); + + var text = File.ReadAllText(path); + var distinctWords = wordsToRender.Distinct().OrderBy(w => w); + + foreach (var w in distinctWords) + clbExcludedWords.Items.Add(w, true); + } + + private void btnChooseFont_Click(object sender, EventArgs e) + { + using var fontDlg = new FontDialog(); + fontDlg.Font = lblFont.Tag as Font ?? this.Font; + if (fontDlg.ShowDialog() == DialogResult.OK) + { + lblFont.Tag = fontDlg.Font; + lblFont.Text = $"{fontDlg.Font.Name}, {fontDlg.Font.Size}pt"; + } + } + + private void btnTextColor_Click(object sender, EventArgs e) + { + using var colorDlg = new ColorDialog(); + if (colorDlg.ShowDialog() == DialogResult.OK) + lblTextColor.BackColor = colorDlg.Color; + } + + private void btnBgColor_Click(object sender, EventArgs e) + { + using var colorDlg = new ColorDialog(); + if (colorDlg.ShowDialog() == DialogResult.OK) + lblBgColor.BackColor = colorDlg.Color; + } + + private void btnGenerate_Click(object sender, EventArgs e) + { + if (string.IsNullOrEmpty(lblFilePath.Text) || !File.Exists(lblFilePath.Text)) + { + MessageBox.Show( + "Пожалуйста, выберите корректный текстовый файл.", + "Ошибка", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + } + + var allWords = clbExcludedWords.Items.Cast(); + + var excluded = allWords + .Where(word => !clbExcludedWords.CheckedItems.Contains(word)) + .ToList(); + + var filters = new List { new BoringWordsFilter(excluded) }; + + var font = lblFont.Tag as Font ?? Font; + var bgColor = lblBgColor.BackColor; + var textColor = lblTextColor.BackColor; + + var canvasSettings = new CanvasSettings() + .SetBackgroundColor(bgColor) + .SetWidth((int)numWidth.Value) + .SetHeight((int)numHeight.Value); + + var textSettings = new TextSettings() + .SetFontFamily(font.FontFamily.Name) + .SetMaxFontSize((int)numMaxSize.Value) + .SetMinFontSize((int)numMinSize.Value) + .SetTextColor(textColor); + + try + { + var bitmap = _generator.Generate( + wordsToRender, + canvasSettings, + textSettings, + filters); + + picturePreview.Image?.Dispose(); + + _generatedBitmap = bitmap; + picturePreview.Image = bitmap; + + btnSave.Enabled = true; + } + catch (Exception ex) + { + MessageBox.Show( + $"Ошибка генерации: {ex.Message}", + "Генерация не удалась", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } + } + + + private void btnSave_Click(object sender, EventArgs e) + { + if (_generatedBitmap == null) + return; + + using var saveDlg = new SaveFileDialog + { + Filter = "PNG Image|*.png|JPEG Image|*.jpg", + Title = "Сохранить облако тегов…", + FileName = "tagcloud.png" + }; + + if (saveDlg.ShowDialog() != DialogResult.OK) + return; + + var ext = Path.GetExtension(saveDlg.FileName).ToLowerInvariant(); + + var format = ext == ".jpg" || ext == ".jpeg" ? ImageFormat.Jpeg : ImageFormat.Png; + + _generatedBitmap.Save(saveDlg.FileName, format); + } + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + btnChooseFile = new Button(); + lblFilePath = new Label(); + groupBoxSettings = new GroupBox(); + lblWidth = new Label(); + numWidth = new NumericUpDown(); + lblHeight = new Label(); + numHeight = new NumericUpDown(); + lblMinSize = new Label(); + numMinSize = new NumericUpDown(); + lblMaxSize = new Label(); + numMaxSize = new NumericUpDown(); + btnChooseFont = new Button(); + lblFont = new Label(); + btnTextColor = new Button(); + btnBgColor = new Button(); + clbExcludedWords = new CheckedListBox(); + btnGenerate = new Button(); + picturePreview = new PictureBox(); + btnSave = new Button(); + lblBgColor = new Label(); + lblTextColor = new Label(); + groupBoxSettings.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)numWidth).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numHeight).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numMinSize).BeginInit(); + ((System.ComponentModel.ISupportInitialize)numMaxSize).BeginInit(); + ((System.ComponentModel.ISupportInitialize)picturePreview).BeginInit(); + SuspendLayout(); + // + // btnChooseFile + // + btnChooseFile.Location = new Point(12, 12); + btnChooseFile.Name = "btnChooseFile"; + btnChooseFile.Size = new Size(120, 30); + btnChooseFile.TabIndex = 0; + btnChooseFile.Text = "Выбрать файл..."; + btnChooseFile.UseVisualStyleBackColor = true; + btnChooseFile.Click += btnChooseFile_Click; + // + // lblFilePath + // + lblFilePath.AutoSize = true; + lblFilePath.Location = new Point(150, 20); + lblFilePath.Name = "lblFilePath"; + lblFilePath.Size = new Size(124, 20); + lblFilePath.TabIndex = 1; + lblFilePath.Text = "Файл не выбран"; + // + // groupBoxSettings + // + groupBoxSettings.Controls.Add(lblWidth); + groupBoxSettings.Controls.Add(numWidth); + groupBoxSettings.Controls.Add(lblHeight); + groupBoxSettings.Controls.Add(numHeight); + groupBoxSettings.Controls.Add(lblMinSize); + groupBoxSettings.Controls.Add(numMinSize); + groupBoxSettings.Controls.Add(lblMaxSize); + groupBoxSettings.Controls.Add(numMaxSize); + groupBoxSettings.Controls.Add(btnChooseFont); + groupBoxSettings.Controls.Add(lblFont); + groupBoxSettings.Location = new Point(12, 60); + groupBoxSettings.Name = "groupBoxSettings"; + groupBoxSettings.Size = new Size(380, 180); + groupBoxSettings.TabIndex = 2; + groupBoxSettings.TabStop = false; + groupBoxSettings.Text = "Настройки генерации"; + // + // lblWidth + // + lblWidth.AutoSize = true; + lblWidth.Location = new Point(10, 25); + lblWidth.Name = "lblWidth"; + lblWidth.Size = new Size(70, 20); + lblWidth.TabIndex = 0; + lblWidth.Text = "Ширина:"; + // + // numWidth + // + numWidth.Location = new Point(80, 23); + numWidth.Maximum = new decimal(new int[] { 4000, 0, 0, 0 }); + numWidth.Minimum = new decimal(new int[] { 100, 0, 0, 0 }); + numWidth.Name = "numWidth"; + numWidth.Size = new Size(80, 27); + numWidth.TabIndex = 1; + numWidth.Value = new decimal(new int[] { 800, 0, 0, 0 }); + // + // lblHeight + // + lblHeight.AutoSize = true; + lblHeight.Location = new Point(180, 25); + lblHeight.Name = "lblHeight"; + lblHeight.Size = new Size(62, 20); + lblHeight.TabIndex = 2; + lblHeight.Text = "Высота:"; + // + // numHeight + // + numHeight.Location = new Point(250, 23); + numHeight.Maximum = new decimal(new int[] { 4000, 0, 0, 0 }); + numHeight.Minimum = new decimal(new int[] { 100, 0, 0, 0 }); + numHeight.Name = "numHeight"; + numHeight.Size = new Size(80, 27); + numHeight.TabIndex = 3; + numHeight.Value = new decimal(new int[] { 600, 0, 0, 0 }); + // + // lblMinSize + // + lblMinSize.AutoSize = true; + lblMinSize.Location = new Point(10, 60); + lblMinSize.Name = "lblMinSize"; + lblMinSize.Size = new Size(218, 20); + lblMinSize.TabIndex = 4; + lblMinSize.Text = "Минимальный размер текста:"; + // + // numMinSize + // + numMinSize.Location = new Point(250, 58); + numMinSize.Maximum = new decimal(new int[] { 200, 0, 0, 0 }); + numMinSize.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + numMinSize.Name = "numMinSize"; + numMinSize.Size = new Size(60, 27); + numMinSize.TabIndex = 5; + numMinSize.Value = new decimal(new int[] { 10, 0, 0, 0 }); + // + // lblMaxSize + // + lblMaxSize.AutoSize = true; + lblMaxSize.Location = new Point(10, 95); + lblMaxSize.Name = "lblMaxSize"; + lblMaxSize.Size = new Size(222, 20); + lblMaxSize.TabIndex = 6; + lblMaxSize.Text = "Максимальный размер текста:"; + // + // numMaxSize + // + numMaxSize.Location = new Point(250, 93); + numMaxSize.Maximum = new decimal(new int[] { 200, 0, 0, 0 }); + numMaxSize.Minimum = new decimal(new int[] { 1, 0, 0, 0 }); + numMaxSize.Name = "numMaxSize"; + numMaxSize.Size = new Size(60, 27); + numMaxSize.TabIndex = 7; + numMaxSize.Value = new decimal(new int[] { 80, 0, 0, 0 }); + numMaxSize.ValueChanged += numMaxSize_ValueChanged; + // + // btnChooseFont + // + btnChooseFont.Location = new Point(10, 135); + btnChooseFont.Name = "btnChooseFont"; + btnChooseFont.Size = new Size(120, 25); + btnChooseFont.TabIndex = 8; + btnChooseFont.Text = "Выбрать шрифт..."; + btnChooseFont.Click += btnChooseFont_Click; + // + // lblFont + // + lblFont.AutoSize = true; + lblFont.Location = new Point(150, 135); + lblFont.Name = "lblFont"; + lblFont.Size = new Size(144, 20); + lblFont.TabIndex = 9; + lblFont.Text = "(шрифт не выбран)"; + // + // btnTextColor + // + btnTextColor.Location = new Point(22, 246); + btnTextColor.Name = "btnTextColor"; + btnTextColor.Size = new Size(120, 25); + btnTextColor.TabIndex = 10; + btnTextColor.Text = "Цвет текста..."; + btnTextColor.Click += btnTextColor_Click; + // + // btnBgColor + // + btnBgColor.Location = new Point(22, 290); + btnBgColor.Name = "btnBgColor"; + btnBgColor.Size = new Size(120, 25); + btnBgColor.TabIndex = 12; + btnBgColor.Text = "Цвет фона..."; + btnBgColor.Click += btnBgColor_Click; + // + // clbExcludedWords + // + clbExcludedWords.CheckOnClick = true; + clbExcludedWords.FormattingEnabled = true; + clbExcludedWords.Location = new Point(22, 338); + clbExcludedWords.Name = "clbExcludedWords"; + clbExcludedWords.Size = new Size(250, 180); + clbExcludedWords.TabIndex = 3; + // + // btnGenerate + // + btnGenerate.Location = new Point(22, 524); + btnGenerate.Name = "btnGenerate"; + btnGenerate.Size = new Size(139, 30); + btnGenerate.TabIndex = 4; + btnGenerate.Text = "Сгенерировать"; + btnGenerate.Click += btnGenerate_Click; + // + // picturePreview + // + picturePreview.BorderStyle = BorderStyle.FixedSingle; + picturePreview.Location = new Point(410, 60); + picturePreview.Name = "picturePreview"; + picturePreview.Size = new Size(688, 534); + picturePreview.SizeMode = PictureBoxSizeMode.Zoom; + picturePreview.TabIndex = 5; + picturePreview.TabStop = false; + // + // btnSave + // + btnSave.Enabled = false; + btnSave.Location = new Point(164, 524); + btnSave.Name = "btnSave"; + btnSave.Size = new Size(110, 30); + btnSave.TabIndex = 6; + btnSave.Text = "Сохранить..."; + btnSave.Click += btnSave_Click; + // + // lblBgColor + // + lblBgColor.AutoSize = true; + lblBgColor.BackColor = Color.White; + lblBgColor.BorderStyle = BorderStyle.FixedSingle; + lblBgColor.Location = new Point(162, 290); + lblBgColor.Name = "lblBgColor"; + lblBgColor.Size = new Size(2, 22); + lblBgColor.TabIndex = 13; + // + // lblTextColor + // + lblTextColor.AutoSize = true; + lblTextColor.BackColor = Color.Black; + lblTextColor.BorderStyle = BorderStyle.FixedSingle; + lblTextColor.Location = new Point(162, 249); + lblTextColor.Name = "lblTextColor"; + lblTextColor.Size = new Size(2, 22); + lblTextColor.TabIndex = 11; + // + // MainForm + // + AutoScaleDimensions = new SizeF(8F, 20F); + AutoScaleMode = AutoScaleMode.Font; + ClientSize = new Size(1110, 660); + Controls.Add(btnChooseFile); + Controls.Add(lblFilePath); + Controls.Add(groupBoxSettings); + Controls.Add(clbExcludedWords); + Controls.Add(btnGenerate); + Controls.Add(picturePreview); + Controls.Add(btnSave); + Controls.Add(btnTextColor); + Controls.Add(btnBgColor); + Controls.Add(lblBgColor); + Controls.Add(lblTextColor); + Name = "MainForm"; + Text = "Генератор облака тегов"; + groupBoxSettings.ResumeLayout(false); + groupBoxSettings.PerformLayout(); + ((System.ComponentModel.ISupportInitialize)numWidth).EndInit(); + ((System.ComponentModel.ISupportInitialize)numHeight).EndInit(); + ((System.ComponentModel.ISupportInitialize)numMinSize).EndInit(); + ((System.ComponentModel.ISupportInitialize)numMaxSize).EndInit(); + ((System.ComponentModel.ISupportInitialize)picturePreview).EndInit(); + ResumeLayout(false); + PerformLayout(); + } + + #endregion + + private Label lblBgColor; + private Label lblTextColor; + } +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.cs b/TagCloudUIClient/MainForm.cs new file mode 100644 index 00000000..e7d44043 --- /dev/null +++ b/TagCloudUIClient/MainForm.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data; +using System.Drawing; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace TagCloudUIClient +{ + public partial class MainForm : Form + { + public MainForm() + { + InitializeComponent(); + } + + private void MainForm_Load(object sender, EventArgs e) + { + + } + + private void numMaxSize_ValueChanged(object sender, EventArgs e) + { + + } + } +} diff --git a/TagCloudUIClient/MainForm.resx b/TagCloudUIClient/MainForm.resx new file mode 100644 index 00000000..8b2ff64a --- /dev/null +++ b/TagCloudUIClient/MainForm.resx @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/TagCloudUIClient/Program.cs b/TagCloudUIClient/Program.cs new file mode 100644 index 00000000..650b4211 --- /dev/null +++ b/TagCloudUIClient/Program.cs @@ -0,0 +1,29 @@ +using Autofac; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.DI; + +namespace TagCloudUIClient +{ + internal static class Program + { + /// + /// The main entry point for the application. + /// + [STAThread] + static void Main() + { + // To customize application configuration such as set high DPI settings or default font, + // see https://aka.ms/applicationconfiguration. + ApplicationConfiguration.Initialize(); + + var builder = new ContainerBuilder(); + builder.RegisterModule(); + builder.RegisterType().As().SingleInstance(); + var container = builder.Build(); + + using var scope = container.BeginLifetimeScope(); + var client = scope.Resolve(); + client.Run(Array.Empty()); + } + } +} \ No newline at end of file diff --git a/TagCloudUIClient/TagCloudUIClient.csproj b/TagCloudUIClient/TagCloudUIClient.csproj new file mode 100644 index 00000000..f78d3be6 --- /dev/null +++ b/TagCloudUIClient/TagCloudUIClient.csproj @@ -0,0 +1,15 @@ + + + + WinExe + net8.0-windows + enable + true + enable + + + + + + + \ No newline at end of file diff --git a/TagCloudUIClient/WinFormsClient.cs b/TagCloudUIClient/WinFormsClient.cs new file mode 100644 index 00000000..eb72fd06 --- /dev/null +++ b/TagCloudUIClient/WinFormsClient.cs @@ -0,0 +1,27 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudUIClient +{ + public class WinFormsClient : IClient + { + private readonly ITagCloudGenerator _generator; + private readonly IReader _reader; + private readonly INormalizer _normalizer; + + public WinFormsClient(ITagCloudGenerator generator, IReader reader, INormalizer normalizer) + { + _generator = generator; + _reader = reader; + _normalizer = normalizer; + } + + public void Run(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + var mainForm = new MainForm(_generator, _reader, _normalizer); + Application.Run(mainForm); + } + } +} From 5913db0b498942ac22fa1210c938ecdfbb1a76bd Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Fri, 19 Dec 2025 03:07:56 +0500 Subject: [PATCH 4/8] Made architecture adjustments and fixes --- TagCloudConsoleClient/ConsoleClient.cs | 17 +++++----- TagCloudConsoleClient/Options.cs | 8 +---- .../TagCloudConsoleClient.csproj | 2 +- .../Algorithms/BasicTagCloudAlgorithm.cs | 2 +- .../Core/Interfaces/IAnalyzer.cs | 2 +- .../Core/Interfaces/IFormatReader.cs | 7 +---- .../Core/Interfaces/INormalizer.cs | 7 +---- TagCloudGenerator/Core/Interfaces/IReader.cs | 11 ------- .../Core/Interfaces/IReaderRepository.cs | 8 +++++ .../Core/Interfaces/ISorterer.cs | 8 +++++ .../Core/Interfaces/ITagCloudAlgorithm.cs | 2 +- .../Core/Services/CloudGenerator.cs | 7 +++-- TagCloudGenerator/DI/TagCloudModule.cs | 7 +++-- .../Analyzers/WordsFrequencyAnalyzer.cs | 12 ++----- .../Filters/BoringWordsFilter.cs | 9 ++---- .../Infrastructure/Readers/DocxReader.cs | 1 + ...CompositeReader.cs => ReaderRepository.cs} | 8 ++--- .../Sorterers/FrequencyDescendingSorterer.cs | 22 +++++++++++++ TagCloudGenerator/Program.cs | 23 -------------- TagCloudGenerator/TagCloudGenerator.csproj | 6 +--- TagCloudGeneratorTests/CloudGeneratorTests.cs | 15 +++++++-- TagCloudGeneratorTests/DocxReaderTests.cs | 5 --- ...eaderTests.cs => ReaderRepositoryTests.cs} | 31 ++++++++++--------- .../WordsFrequencyAnalyzerTests.cs | 16 +++++----- TagCloudUIClient/MainForm.Designer.cs | 23 ++++++++++---- TagCloudUIClient/TagCloudUIClient.csproj | 2 +- TagCloudUIClient/WinFormsClient.cs | 6 ++-- di.sln | 15 +++++++++ 28 files changed, 147 insertions(+), 135 deletions(-) delete mode 100644 TagCloudGenerator/Core/Interfaces/IReader.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IReaderRepository.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ISorterer.cs rename TagCloudGenerator/Infrastructure/Readers/{CompositeReader.cs => ReaderRepository.cs} (73%) create mode 100644 TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs delete mode 100644 TagCloudGenerator/Program.cs rename TagCloudGeneratorTests/{CompositeReaderTests.cs => ReaderRepositoryTests.cs} (58%) diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index 81e39111..e1fbd794 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -1,11 +1,6 @@ using CommandLine; -using System; -using System.Collections.Generic; using System.Drawing; using System.Drawing.Imaging; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; @@ -15,14 +10,14 @@ public class ConsoleClient : IClient { private readonly ITagCloudGenerator _generator; private readonly IEnumerable _filters; - private readonly IReader _reader; + private readonly IReaderRepository _readersRepository; private readonly INormalizer _normalizer; - public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReader reader, INormalizer normalizer) + public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReaderRepository readers, INormalizer normalizer) { _generator = generator; _filters = filters; - _reader = reader; + _readersRepository = readers; _normalizer = normalizer; } @@ -61,10 +56,14 @@ private void RunWithOptions(Options opts) try { - var words = _normalizer.Normalize(_reader.TryRead(inputFile)); + var reader = _readersRepository.TryGetReader(inputFile); + + var words = _normalizer.Normalize(reader.TryRead(inputFile)); var image = _generator.Generate(words, canvasSettings, textSettings, _filters); if(image != null) image.Save(outputFile, ImageFormat.Png); + image.Dispose(); + Console.WriteLine("Tag cloud generation completed successfully!"); } catch (Exception ex) diff --git a/TagCloudConsoleClient/Options.cs b/TagCloudConsoleClient/Options.cs index df1790fa..2c5220cf 100644 --- a/TagCloudConsoleClient/Options.cs +++ b/TagCloudConsoleClient/Options.cs @@ -1,10 +1,4 @@ -using CommandLine.Text; -using CommandLine; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using CommandLine; namespace TagCloudConsoleClient { diff --git a/TagCloudConsoleClient/TagCloudConsoleClient.csproj b/TagCloudConsoleClient/TagCloudConsoleClient.csproj index d1c033ae..e44d6f1d 100644 --- a/TagCloudConsoleClient/TagCloudConsoleClient.csproj +++ b/TagCloudConsoleClient/TagCloudConsoleClient.csproj @@ -4,7 +4,7 @@ Exe net8.0 enable - enable + disable diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs index f856af9d..403a5bf1 100644 --- a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -91,7 +91,7 @@ public BasicTagCloudAlgorithm WithCenterAt(Point point) return this; } - public BasicTagCloudAlgorithm Reset() + public ITagCloudAlgorithm Reset() { rectangles.Clear(); return this; diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs index 332b7136..8c47d59d 100644 --- a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -4,6 +4,6 @@ namespace TagCloudGenerator.Core.Interfaces { public interface IAnalyzer { - List<(string Word, int Frequency)> Analyze(List words); + Dictionary Analyze(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs index 5be6f11d..1d9e1fbf 100644 --- a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace TagCloudGenerator.Core.Interfaces { public interface IFormatReader diff --git a/TagCloudGenerator/Core/Interfaces/INormalizer.cs b/TagCloudGenerator/Core/Interfaces/INormalizer.cs index ef2224f4..ebb6482b 100644 --- a/TagCloudGenerator/Core/Interfaces/INormalizer.cs +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - + namespace TagCloudGenerator.Core.Interfaces { public interface INormalizer diff --git a/TagCloudGenerator/Core/Interfaces/IReader.cs b/TagCloudGenerator/Core/Interfaces/IReader.cs deleted file mode 100644 index 991c7d17..00000000 --- a/TagCloudGenerator/Core/Interfaces/IReader.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace TagCloudGenerator.Core.Interfaces -{ - public interface IReader - { - List TryRead(string filePath); - bool CanRead(string filePath); - } -} diff --git a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs new file mode 100644 index 00000000..8403e4bb --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IReaderRepository + { + public IFormatReader TryGetReader(string filePath); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ISorterer.cs b/TagCloudGenerator/Core/Interfaces/ISorterer.cs new file mode 100644 index 00000000..b504e89d --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ISorterer.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ISorterer + { + public List<(string Word, int Frequency)> Sort(Dictionary wordsWithFreqs); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs index 50b2cdd9..1331e9b8 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -7,6 +7,6 @@ public interface ITagCloudAlgorithm { Rectangle PutNextRectangle(Size rectangleSize); - public BasicTagCloudAlgorithm Reset(); + public ITagCloudAlgorithm Reset(); } } diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index 448c6036..eed1ef26 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -13,19 +13,22 @@ public class CloudGenerator : ITagCloudGenerator private readonly IRenderer _renderer; private readonly IFontSizeCalculator _fontSizeCalculator; private readonly ITextMeasurer _textMeasurer; + private readonly ISorterer _sorterer; private readonly Point _center; public CloudGenerator(ITagCloudAlgorithm algorithm, IAnalyzer analyzer, IRenderer renderer, IFontSizeCalculator fontSizeCalculator, - ITextMeasurer textMeasurer) + ITextMeasurer textMeasurer, + ISorterer sorterer) { this._algorithm = algorithm; _analyzer = analyzer; _renderer = renderer; _fontSizeCalculator = fontSizeCalculator; _textMeasurer = textMeasurer; + _sorterer = sorterer; _center = new Point(0, 0); } @@ -34,7 +37,7 @@ public CloudGenerator(ITagCloudAlgorithm algorithm, words = ApplyFilters(words, filters); if(words.Count == 0) return null; - var wordsWithFreq = _analyzer.Analyze(words); + var wordsWithFreq = _sorterer.Sort(_analyzer.Analyze(words)); var initializedItems = InitializeCloudItems(wordsWithFreq, textSettings).ToList(); diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs index 67553bb7..3df3fe70 100644 --- a/TagCloudGenerator/DI/TagCloudModule.cs +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -9,6 +9,7 @@ using TagCloudGenerator.Infrastructure.Normalizers; using TagCloudGenerator.Infrastructure.Readers; using TagCloudGenerator.Infrastructure.Renderers; +using TagCloudGenerator.Infrastructure.Sorterers; namespace TagCloudGenerator.DI { @@ -19,10 +20,12 @@ protected override void Load(ContainerBuilder builder) builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType() - .As() + builder.RegisterType() + .As() .SingleInstance(); + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As(); builder.RegisterType().As(); diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index 90e5cf35..11fd06cb 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -6,12 +6,12 @@ namespace TagCloudGenerator.Infrastructure.Analyzers { public class WordsFrequencyAnalyzer : IAnalyzer { - public List<(string Word, int Frequency)> Analyze(List words) + public Dictionary Analyze(List words) { var wordFreqDictionary = new Dictionary(); if (words == null || words.Count == 0) - return new List<(string word, int freq)>(); + return new Dictionary(); foreach (string word in words) { @@ -19,13 +19,7 @@ public class WordsFrequencyAnalyzer : IAnalyzer else wordFreqDictionary[word]++; } - var result = wordFreqDictionary - .OrderByDescending(pair => pair.Value) - .ThenBy(pair => pair.Key) - .Select(pair => (pair.Key, pair.Value)) - .ToList(); - - return result; + return wordFreqDictionary; } } } diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs index 56589740..4cfdbd03 100644 --- a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -8,14 +8,9 @@ public class BoringWordsFilter : IFilter public BoringWordsFilter() { } - public BoringWordsFilter(string[] words) + public BoringWordsFilter(IEnumerable words) { - boringWords = words; - } - - public BoringWordsFilter(List wordsList) - { - boringWords = wordsList.ToArray(); + boringWords = words.ToArray(); } public List Filter(List words) diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 81e95317..76110bbc 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -16,6 +16,7 @@ public List TryRead(string filePath) try { using var doc = WordprocessingDocument.Open(filePath, false); + if (doc == null) return new List(); return doc.MainDocumentPart.Document.Body .Elements() diff --git a/TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs similarity index 73% rename from TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs rename to TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs index f2639749..027dbcdd 100644 --- a/TagCloudGenerator/Infrastructure/Readers/CompositeReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -2,11 +2,11 @@ namespace TagCloudGenerator.Infrastructure.Readers { - public class CompositeReader : IReader + public class ReaderRepository : IReaderRepository { private readonly IEnumerable _readers; - public CompositeReader(IEnumerable readers) + public ReaderRepository(IEnumerable readers) { _readers = readers; } @@ -16,13 +16,13 @@ public bool CanRead(string filePath) return _readers.Any(r => r.CanRead(filePath)); } - public List TryRead(string filePath) + public IFormatReader TryGetReader(string filePath) { var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); if (reader == null) throw new NotSupportedException("Формат не поддерживается"); - return reader.TryRead(filePath); + return reader; } } } \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs new file mode 100644 index 00000000..03190901 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs @@ -0,0 +1,22 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Sorterers +{ + /// + /// Тут пожалуй отвечу насчет использования списка кортежей, насколько я выяснил в интернете, в целом словари можно сортировать, + /// но вообще говоря Dictionary не гарантирует верный порядок элементов из-за особенностей своего устройства + /// В большинстве случаев - да, будет сортировать, но гарантий нам никто не дает, особенно на больших словарях + /// есть SortedDictionary, но он сортирует по ключу, поэтому я решил пойти таким путем - отделить сортировку от анализа частот + /// + public class FrequencyDescendingSorterer : ISorterer + { + public List<(string Word, int Frequency)> Sort(Dictionary wordsWithFreqs) + { + return wordsWithFreqs + .OrderByDescending(w => w.Value) + .ThenBy(w => w.Key) + .Select(kvpair => (kvpair.Key, kvpair.Value)) + .ToList(); + } + } +} diff --git a/TagCloudGenerator/Program.cs b/TagCloudGenerator/Program.cs deleted file mode 100644 index eb41ef57..00000000 --- a/TagCloudGenerator/Program.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using Autofac; -using TagCloudGenerator.Core.Interfaces; -using TagCloudGenerator.DI; - -namespace TagCloudGenerator -{ - public class Program - { - public static void Main(string[] args) - { - var builder = new ContainerBuilder(); - - builder.RegisterModule(); - - var container = builder.Build(); - - using var scope = container.BeginLifetimeScope(); - var client = scope.Resolve(); - client.Run(args); - } - } -} \ No newline at end of file diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj index 4fc29757..2622d6f3 100644 --- a/TagCloudGenerator/TagCloudGenerator.csproj +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -1,16 +1,12 @@  - Exe + Library net8.0 enable disable - - - - diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index f357a601..9b7eafe5 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -15,6 +15,7 @@ public class CloudGeneratorTests private Mock rendererMock; private Mock fontSizeCalculatorMock; private Mock textMeasurerMock; + private Mock sortererMock; private CloudGenerator cloudGenerator; [SetUp] @@ -26,13 +27,15 @@ public void Setup() rendererMock = new Mock(); fontSizeCalculatorMock = new Mock(); textMeasurerMock = new Mock(); + sortererMock = new Mock(); cloudGenerator = new CloudGenerator( algorithmMock.Object, analyzerMock.Object, rendererMock.Object, fontSizeCalculatorMock.Object, - textMeasurerMock.Object); + textMeasurerMock.Object, + sortererMock.Object); } [Test] @@ -87,7 +90,9 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() var words = new List { "hello", "world", "hello" }; var filteredWords = new List { "hello", "world", "hello" }; - var analyzed = new List<(string Word, int Frequency)> { ("hello", 2), ("world", 1) }; + var analyzed = new Dictionary { { "hello", 2 }, { "world", 1 } }; + + var sorted = new List<(string Word, int Frequency)> { ("hello", 2), ("world", 1) }; filterMock .Setup(f => f.Filter(words)) @@ -97,6 +102,10 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() .Setup(a => a.Analyze(filteredWords)) .Returns(analyzed); + sortererMock + .Setup(s => s.Sort(analyzed)) + .Returns(sorted); + var textSettings = new TextSettings() .SetFontFamily("Arial") .SetFontSizeRange(12, 72); @@ -136,6 +145,8 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() Assert.IsNotNull(result); + result.Dispose(); + filterMock.Verify(f => f.Filter(words), Times.Once); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); algorithmMock.Verify(a => a.Reset(), Times.Once); diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs index f4dc74b8..3d20805d 100644 --- a/TagCloudGeneratorTests/DocxReaderTests.cs +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -1,11 +1,6 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using NUnit.Framework; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using TagCloudGenerator.Infrastructure.Readers; namespace TagCloudGeneratorTests diff --git a/TagCloudGeneratorTests/CompositeReaderTests.cs b/TagCloudGeneratorTests/ReaderRepositoryTests.cs similarity index 58% rename from TagCloudGeneratorTests/CompositeReaderTests.cs rename to TagCloudGeneratorTests/ReaderRepositoryTests.cs index 738d3059..8da2cce7 100644 --- a/TagCloudGeneratorTests/CompositeReaderTests.cs +++ b/TagCloudGeneratorTests/ReaderRepositoryTests.cs @@ -5,29 +5,29 @@ namespace TagCloudGeneratorTests { - public class CompositeReaderTests + public class ReaderRepositoryTests { - private Mock reader1Mock; + private Mock firstReaderMock; private Mock reader2Mock; - private CompositeReader compositeReader; + private ReaderRepository readerRepository; [SetUp] public void Setup() { - reader1Mock = new Mock(); + firstReaderMock = new Mock(); reader2Mock = new Mock(); - compositeReader = new CompositeReader( - new[] { reader1Mock.Object, reader2Mock.Object }); + readerRepository = new ReaderRepository( + new[] { firstReaderMock.Object, reader2Mock.Object }); } [Test] public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() { - reader1Mock.Setup(r => r.CanRead("file.docx")).Returns(false); + firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); - var result = compositeReader.CanRead("file.docx"); + var result = readerRepository.CanRead("file.docx"); Assert.That(result, Is.True); } @@ -35,10 +35,10 @@ public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() [Test] public void CanRead_WhenNoReaderCanRead_ReturnsFalse() { - reader1Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); - var result = compositeReader.CanRead("file.unknown"); + var result = readerRepository.CanRead("file.unknown"); Assert.That(result, Is.False); } @@ -48,26 +48,27 @@ public void TryRead_UsesFirstMatchingReader() { var expected = new List { "word1", "word2" }; - reader1Mock.Setup(r => r.CanRead("file.docx")).Returns(false); + firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); reader2Mock.Setup(r => r.TryRead("file.docx")).Returns(expected); - var result = compositeReader.TryRead("file.docx"); + var reader = readerRepository.TryGetReader("file.docx"); + var result = reader.TryRead("file.docx"); Assert.That(result, Is.EqualTo(expected)); reader2Mock.Verify(r => r.TryRead("file.docx"), Times.Once); - reader1Mock.Verify(r => r.TryRead(It.IsAny()), Times.Never); + firstReaderMock.Verify(r => r.TryRead(It.IsAny()), Times.Never); } [Test] public void TryRead_WhenNoReaderFound_ThrowsException() { - reader1Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); Assert.Throws(() => - compositeReader.TryRead("file.xyz")); + readerRepository.TryGetReader("file.xyz")); } } } diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index c3bf6f39..f5eb2c55 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -22,8 +22,8 @@ public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(1)); - Assert.That(result[0].Word, Is.EqualTo("hello")); - Assert.That(result[0].Frequency, Is.EqualTo(1)); + Assert.That(result[0].Key, Is.EqualTo("hello")); + Assert.That(result[0].Value, Is.EqualTo(1)); } [Test] @@ -33,12 +33,12 @@ public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() var result = frequencyAnalyzer.Analyze(words).ToList(); Assert.That(result.Count, Is.EqualTo(3)); - Assert.That(result[0].Word, Is.EqualTo("hello")); - Assert.That(result[0].Frequency, Is.EqualTo(3)); - Assert.That(result[1].Word, Is.EqualTo("world")); - Assert.That(result[1].Frequency, Is.EqualTo(2)); - Assert.That(result[2].Word, Is.EqualTo("test")); - Assert.That(result[2].Frequency, Is.EqualTo(1)); + Assert.That(result[0].Key, Is.EqualTo("hello")); + Assert.That(result[0].Value, Is.EqualTo(3)); + Assert.That(result[1].Key, Is.EqualTo("world")); + Assert.That(result[1].Value, Is.EqualTo(2)); + Assert.That(result[2].Key, Is.EqualTo("test")); + Assert.That(result[2].Value, Is.EqualTo(1)); } [Test] diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs index 90154979..bbf6b47a 100644 --- a/TagCloudUIClient/MainForm.Designer.cs +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -13,7 +13,7 @@ public partial class MainForm : Form private System.ComponentModel.IContainer components = null; private readonly ITagCloudGenerator _generator; - private readonly IReader _reader; + private readonly IReaderRepository _readersRepository; private readonly INormalizer _normalizer; private string _lastOutputPath = string.Empty; @@ -42,10 +42,10 @@ public partial class MainForm : Form private Button btnSave; - public MainForm(ITagCloudGenerator generator, IReader reader, INormalizer normalizer) + public MainForm(ITagCloudGenerator generator, IReaderRepository readers, INormalizer normalizer) { _generator = generator; - _reader = reader; + _readersRepository = readers; _normalizer = normalizer; InitializeComponent(); } @@ -64,9 +64,20 @@ private void btnChooseFile_Click(object sender, EventArgs e) private void LoadExcludedWords(string path) { clbExcludedWords.Items.Clear(); - - var words = _reader.TryRead(path); - wordsToRender = _normalizer.Normalize(words); + try + { + var reader = _readersRepository.TryGetReader(path); + var words = reader.TryRead(path); + wordsToRender = _normalizer.Normalize(words); + } + catch(Exception ex) + { + MessageBox.Show( + $"Ошибка чтения: {ex.Message}", + "Формат файла не поддерживается", + MessageBoxButtons.OK, + MessageBoxIcon.Error); + } var text = File.ReadAllText(path); var distinctWords = wordsToRender.Distinct().OrderBy(w => w); diff --git a/TagCloudUIClient/TagCloudUIClient.csproj b/TagCloudUIClient/TagCloudUIClient.csproj index f78d3be6..a1c082b4 100644 --- a/TagCloudUIClient/TagCloudUIClient.csproj +++ b/TagCloudUIClient/TagCloudUIClient.csproj @@ -3,7 +3,7 @@ WinExe net8.0-windows - enable + disable true enable diff --git a/TagCloudUIClient/WinFormsClient.cs b/TagCloudUIClient/WinFormsClient.cs index eb72fd06..c0ffe1b5 100644 --- a/TagCloudUIClient/WinFormsClient.cs +++ b/TagCloudUIClient/WinFormsClient.cs @@ -5,13 +5,13 @@ namespace TagCloudUIClient public class WinFormsClient : IClient { private readonly ITagCloudGenerator _generator; - private readonly IReader _reader; + private readonly IReaderRepository _reader; private readonly INormalizer _normalizer; - public WinFormsClient(ITagCloudGenerator generator, IReader reader, INormalizer normalizer) + public WinFormsClient(ITagCloudGenerator generator, IReaderRepository repository, INormalizer normalizer) { _generator = generator; - _reader = reader; + _reader = repository; _normalizer = normalizer; } diff --git a/di.sln b/di.sln index 9f12a7c7..8553282a 100644 --- a/di.sln +++ b/di.sln @@ -9,6 +9,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGenerator", "TagClo EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGeneratorTests", "TagCloudGeneratorTests\TagCloudGeneratorTests.csproj", "{235C2379-72BC-4BF2-994D-B5DA4146AB8D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudConsoleClient", "TagCloudConsoleClient\TagCloudConsoleClient.csproj", "{C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudUIClient", "TagCloudUIClient\TagCloudUIClient.csproj", "{6415D8CF-C1CB-4B75-AA1D-C0207B79020C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -27,8 +31,19 @@ Global {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.ActiveCfg = Release|Any CPU {235C2379-72BC-4BF2-994D-B5DA4146AB8D}.Release|Any CPU.Build.0 = Release|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.Build.0 = Release|Any CPU + {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {BFEBD95D-6AE8-4FB5-A3F5-52EAB5899DAC} + EndGlobalSection EndGlobal From ebc9ca07b8c85eb6e6603e977326a386760737fe Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Fri, 26 Dec 2025 02:14:10 +0500 Subject: [PATCH 5/8] Updated FrequencyAnalyzer and ReaderRepository with DocxReader --- TagCloudConsoleClient/ConsoleClient.cs | 19 ++++++++++++------- .../TagCloudConsoleClient.csproj | 2 +- .../Core/Interfaces/IReaderRepository.cs | 2 +- .../Analyzers/WordsFrequencyAnalyzer.cs | 3 ++- .../Infrastructure/Readers/DocxReader.cs | 2 -- .../Readers/ReaderRepository.cs | 10 +++++----- .../Sorterers/FrequencyDescendingSorterer.cs | 6 ------ TagCloudGenerator/TagCloudGenerator.csproj | 2 +- TagCloudUIClient/MainForm.Designer.cs | 12 +++++++++--- 9 files changed, 31 insertions(+), 27 deletions(-) diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index e1fbd794..be20359e 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -56,15 +56,20 @@ private void RunWithOptions(Options opts) try { - var reader = _readersRepository.TryGetReader(inputFile); + if(_readersRepository.TryGetReader(inputFile, out var outputReader)) + { + var words = _normalizer.Normalize(outputReader.TryRead(inputFile)); + var image = _generator.Generate(words, canvasSettings, textSettings, _filters); - var words = _normalizer.Normalize(reader.TryRead(inputFile)); - var image = _generator.Generate(words, canvasSettings, textSettings, _filters); + if (image != null) image.Save(outputFile, ImageFormat.Png); + image.Dispose(); - if(image != null) image.Save(outputFile, ImageFormat.Png); - image.Dispose(); - - Console.WriteLine("Tag cloud generation completed successfully!"); + Console.WriteLine("Tag cloud generation completed successfully!"); + } + else + { + throw new Exception("no suitable formate readers found"); + } } catch (Exception ex) { diff --git a/TagCloudConsoleClient/TagCloudConsoleClient.csproj b/TagCloudConsoleClient/TagCloudConsoleClient.csproj index e44d6f1d..f4c4f3d1 100644 --- a/TagCloudConsoleClient/TagCloudConsoleClient.csproj +++ b/TagCloudConsoleClient/TagCloudConsoleClient.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net8.0-windows enable disable diff --git a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs index 8403e4bb..a52d680a 100644 --- a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -3,6 +3,6 @@ namespace TagCloudGenerator.Core.Interfaces { public interface IReaderRepository { - public IFormatReader TryGetReader(string filePath); + public bool TryGetReader(string filePath, out IFormatReader outputReader); } } diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index 11fd06cb..f6308382 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -15,7 +15,8 @@ public Dictionary Analyze(List words) foreach (string word in words) { - if (!wordFreqDictionary.ContainsKey(word)) wordFreqDictionary.Add(word, 1); + if(wordFreqDictionary.TryAdd(word, 1)) + continue; else wordFreqDictionary[word]++; } diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 76110bbc..8126d4c9 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -16,8 +16,6 @@ public List TryRead(string filePath) try { using var doc = WordprocessingDocument.Open(filePath, false); - if (doc == null) return new List(); - return doc.MainDocumentPart.Document.Body .Elements() .Select(p => p.InnerText) diff --git a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs index 027dbcdd..dfc8e973 100644 --- a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -16,13 +16,13 @@ public bool CanRead(string filePath) return _readers.Any(r => r.CanRead(filePath)); } - public IFormatReader TryGetReader(string filePath) + public bool TryGetReader(string filePath, out IFormatReader outputReader) { - var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); - if (reader == null) - throw new NotSupportedException("Формат не поддерживается"); + outputReader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + if (outputReader == null) + return false; - return reader; + return true; } } } \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs index 03190901..0bdc20c3 100644 --- a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs +++ b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs @@ -2,12 +2,6 @@ namespace TagCloudGenerator.Infrastructure.Sorterers { - /// - /// Тут пожалуй отвечу насчет использования списка кортежей, насколько я выяснил в интернете, в целом словари можно сортировать, - /// но вообще говоря Dictionary не гарантирует верный порядок элементов из-за особенностей своего устройства - /// В большинстве случаев - да, будет сортировать, но гарантий нам никто не дает, особенно на больших словарях - /// есть SortedDictionary, но он сортирует по ключу, поэтому я решил пойти таким путем - отделить сортировку от анализа частот - /// public class FrequencyDescendingSorterer : ISorterer { public List<(string Word, int Frequency)> Sort(Dictionary wordsWithFreqs) diff --git a/TagCloudGenerator/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj index 2622d6f3..3937f9c9 100644 --- a/TagCloudGenerator/TagCloudGenerator.csproj +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -2,7 +2,7 @@ Library - net8.0 + net8.0-windows enable disable diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs index bbf6b47a..4a4155e4 100644 --- a/TagCloudUIClient/MainForm.Designer.cs +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -66,9 +66,15 @@ private void LoadExcludedWords(string path) clbExcludedWords.Items.Clear(); try { - var reader = _readersRepository.TryGetReader(path); - var words = reader.TryRead(path); - wordsToRender = _normalizer.Normalize(words); + if (_readersRepository.TryGetReader(path, out var outputReader)) + { + var words = outputReader.TryRead(path); + wordsToRender = _normalizer.Normalize(words); + } + else + { + throw new Exception("no suitable formate readers found"); + } } catch(Exception ex) { From d68665abc66a822b394c28f5669ac3dbf3c95e9c Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Fri, 26 Dec 2025 02:20:35 +0500 Subject: [PATCH 6/8] Updated ReaderRepositoryTests --- .../ReaderRepositoryTests.cs | 29 ++++++++++--------- .../TagCloudGeneratorTests.csproj | 2 +- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/TagCloudGeneratorTests/ReaderRepositoryTests.cs b/TagCloudGeneratorTests/ReaderRepositoryTests.cs index 8da2cce7..ec1761ae 100644 --- a/TagCloudGeneratorTests/ReaderRepositoryTests.cs +++ b/TagCloudGeneratorTests/ReaderRepositoryTests.cs @@ -8,24 +8,24 @@ namespace TagCloudGeneratorTests public class ReaderRepositoryTests { private Mock firstReaderMock; - private Mock reader2Mock; + private Mock secondReaderMock; private ReaderRepository readerRepository; [SetUp] public void Setup() { firstReaderMock = new Mock(); - reader2Mock = new Mock(); + secondReaderMock = new Mock(); readerRepository = new ReaderRepository( - new[] { firstReaderMock.Object, reader2Mock.Object }); + new[] { firstReaderMock.Object, secondReaderMock.Object }); } [Test] public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() { firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); - reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); + secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); var result = readerRepository.CanRead("file.docx"); @@ -36,7 +36,7 @@ public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() public void CanRead_WhenNoReaderCanRead_ReturnsFalse() { firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); - reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); var result = readerRepository.CanRead("file.unknown"); @@ -47,17 +47,20 @@ public void CanRead_WhenNoReaderCanRead_ReturnsFalse() public void TryRead_UsesFirstMatchingReader() { var expected = new List { "word1", "word2" }; + var result = new List(); firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); - reader2Mock.Setup(r => r.CanRead("file.docx")).Returns(true); - reader2Mock.Setup(r => r.TryRead("file.docx")).Returns(expected); + secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); + secondReaderMock.Setup(r => r.TryRead("file.docx")).Returns(expected); - var reader = readerRepository.TryGetReader("file.docx"); - var result = reader.TryRead("file.docx"); + if(readerRepository.TryGetReader("file.docx", out var reader)) + { + result = reader.TryRead("file.docx"); + } Assert.That(result, Is.EqualTo(expected)); - reader2Mock.Verify(r => r.TryRead("file.docx"), Times.Once); + secondReaderMock.Verify(r => r.TryRead("file.docx"), Times.Once); firstReaderMock.Verify(r => r.TryRead(It.IsAny()), Times.Never); } @@ -65,10 +68,10 @@ public void TryRead_UsesFirstMatchingReader() public void TryRead_WhenNoReaderFound_ThrowsException() { firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); - reader2Mock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); - Assert.Throws(() => - readerRepository.TryGetReader("file.xyz")); + Assert.That(readerRepository.TryGetReader("file.xyz", out var reader), Is.False); + Assert.That(reader, Is.Null); } } } diff --git a/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj index 3480a5e0..94cb41bc 100644 --- a/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj +++ b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj @@ -1,7 +1,7 @@ - net8.0 + net8.0-windows enable enable From 3b47dffe76827028aa5aa4ca3b72d860f22a1e90 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 27 Dec 2025 06:20:36 +0500 Subject: [PATCH 7/8] Implemented Result pattern --- TagCloudConsoleClient/ConsoleClient.cs | 34 ++--- TagCloudGenerator/Core/Interfaces/IClient.cs | 3 +- .../Core/Interfaces/IFormatReader.cs | 5 +- .../Core/Interfaces/INormalizer.cs | 3 +- .../Core/Interfaces/IReaderRepository.cs | 7 +- .../Core/Interfaces/IRenderer.cs | 3 +- .../Core/Interfaces/ITagCloudGenerator.cs | 3 +- .../Core/Services/CloudGenerator.cs | 5 +- .../Infrastructure/Readers/DocxReader.cs | 12 +- .../Readers/ReaderRepository.cs | 15 +- .../Infrastructure/Readers/TxtReader.cs | 12 +- .../Infrastructure/Renderers/PngRenderer.cs | 60 ++++++-- TagCloudGenerator/Infrastructure/Result.cs | 140 ++++++++++++++++++ TagCloudUIClient/MainForm.Designer.cs | 77 +++++----- TagCloudUIClient/MainForm.cs | 4 +- TagCloudUIClient/MainForm.resx | 54 +++---- TagCloudUIClient/TagCloudUIClient.csproj | 2 +- di.sln | 10 +- 18 files changed, 299 insertions(+), 150 deletions(-) create mode 100644 TagCloudGenerator/Infrastructure/Result.cs diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index be20359e..51cc891c 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -3,6 +3,8 @@ using System.Drawing.Imaging; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using static System.Runtime.InteropServices.JavaScript.JSType; namespace TagCloudConsoleClient { @@ -10,14 +12,14 @@ public class ConsoleClient : IClient { private readonly ITagCloudGenerator _generator; private readonly IEnumerable _filters; - private readonly IReaderRepository _readersRepository; + private readonly IReaderRepository _readerRepository; private readonly INormalizer _normalizer; public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReaderRepository readers, INormalizer normalizer) { _generator = generator; _filters = filters; - _readersRepository = readers; + _readerRepository = readers; _normalizer = normalizer; } @@ -54,27 +56,17 @@ private void RunWithOptions(Options opts) Console.WriteLine($"Input file: {inputFile}"); Console.WriteLine($"Output file: {outputFile}"); - try - { - if(_readersRepository.TryGetReader(inputFile, out var outputReader)) - { - var words = _normalizer.Normalize(outputReader.TryRead(inputFile)); - var image = _generator.Generate(words, canvasSettings, textSettings, _filters); - - if (image != null) image.Save(outputFile, ImageFormat.Png); - image.Dispose(); - Console.WriteLine("Tag cloud generation completed successfully!"); - } - else + _readerRepository.TryGetReader(inputFile) + .Then(reader => reader.TryRead(inputFile)) + .Then(words => _normalizer.Normalize(words)) + .Then(words => _generator.Generate(words, canvasSettings, textSettings, _filters)) + .Then(image => Result.OfAction(() => image.Save(outputFile, ImageFormat.Png), "Failed to save output image")) + .OnFail(error => { - throw new Exception("no suitable formate readers found"); - } - } - catch (Exception ex) - { - Console.WriteLine($"Error during generation: {ex.Message}"); - } + Console.WriteLine("Error during generation:"); + Console.WriteLine(error); + }); } private static Color? TryParseColor(string colorStr) diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs index 20ce48af..de0e1098 100644 --- a/TagCloudGenerator/Core/Interfaces/IClient.cs +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -1,5 +1,4 @@ - -namespace TagCloudGenerator.Core.Interfaces +namespace TagCloudGenerator.Core.Interfaces { public interface IClient { diff --git a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs index 1d9e1fbf..0575196d 100644 --- a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -1,9 +1,10 @@ - +using TagCloudGenerator.Infrastructure; + namespace TagCloudGenerator.Core.Interfaces { public interface IFormatReader { bool CanRead(string filePath); - List TryRead(string filePath); + Result> TryRead(string filePath); } } diff --git a/TagCloudGenerator/Core/Interfaces/INormalizer.cs b/TagCloudGenerator/Core/Interfaces/INormalizer.cs index ebb6482b..daaaf97b 100644 --- a/TagCloudGenerator/Core/Interfaces/INormalizer.cs +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -1,5 +1,4 @@ - -namespace TagCloudGenerator.Core.Interfaces +namespace TagCloudGenerator.Core.Interfaces { public interface INormalizer { diff --git a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs index a52d680a..83f5ba47 100644 --- a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -1,8 +1,9 @@ - +using TagCloudGenerator.Infrastructure; + namespace TagCloudGenerator.Core.Interfaces { public interface IReaderRepository { - public bool TryGetReader(string filePath, out IFormatReader outputReader); + public Result TryGetReader(string filePath); } -} +} \ No newline at end of file diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs index 990f5dcd..b4eea0d2 100644 --- a/TagCloudGenerator/Core/Interfaces/IRenderer.cs +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -1,10 +1,11 @@ using System.Drawing; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface IRenderer { - public Bitmap Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings); + public Result Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs index 5bb4c556..429ab2f4 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -1,10 +1,11 @@ using System.Drawing; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudGenerator { - public Bitmap? Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters); + public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters); } } diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index eed1ef26..7bba0947 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -2,6 +2,7 @@ using TagCloudGenerator.Algorithms; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudGenerator.Core.Services @@ -32,10 +33,10 @@ public CloudGenerator(ITagCloudAlgorithm algorithm, _center = new Point(0, 0); } - public Bitmap? Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) + public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) { words = ApplyFilters(words, filters); - if(words.Count == 0) return null; + if(words.Count == 0) return Result.Fail("Found no words to render after filtering"); var wordsWithFreq = _sorterer.Sort(_analyzer.Analyze(words)); diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 8126d4c9..6df98a7b 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -11,22 +11,18 @@ public bool CanRead(string filePath) return Path.GetExtension(filePath).Equals(".docx", StringComparison.OrdinalIgnoreCase); } - public List TryRead(string filePath) + public Result> TryRead(string filePath) { - try + return Result.Of(() => { using var doc = WordprocessingDocument.Open(filePath, false); + return doc.MainDocumentPart.Document.Body .Elements() .Select(p => p.InnerText) .Where(t => !string.IsNullOrWhiteSpace(t)) .ToList(); - } - catch (Exception e) - { - Console.WriteLine($"DOCX read error: {e.Message}"); - return new List(); - } + }, "Failed to read DOCX file"); } } } diff --git a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs index dfc8e973..9110764e 100644 --- a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -16,13 +16,18 @@ public bool CanRead(string filePath) return _readers.Any(r => r.CanRead(filePath)); } - public bool TryGetReader(string filePath, out IFormatReader outputReader) + public Result TryGetReader(string filePath) { - outputReader = _readers.FirstOrDefault(r => r.CanRead(filePath)); - if (outputReader == null) - return false; + if (string.IsNullOrWhiteSpace(filePath)) + return Result.Fail("File path is empty"); - return true; + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + + if (reader == null) + return Result.Fail( + $"No reader found for file '{filePath}'"); + + return Result.Ok(reader); } } } \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs index ede68db2..1d5a5461 100644 --- a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs @@ -9,17 +9,9 @@ public bool CanRead(string filePath) return Path.GetExtension(filePath).Equals(".txt", StringComparison.OrdinalIgnoreCase); } - public List TryRead(string filePath) + public Result> TryRead(string filePath) { - try - { - return File.ReadAllLines(filePath).ToList(); - } - catch(IOException e) - { - Console.WriteLine($"Error reading file: {e.Message}"); - return null; - } + return Result.Of(() => { return File.ReadAllLines(filePath).ToList(); }, "Faield to read .txt file"); } } } diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index e44fddfd..124b9c95 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -7,29 +7,45 @@ namespace TagCloudGenerator.Infrastructure.Renderers { public class PngRenderer : IRenderer { - public Bitmap Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) + public Result Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) { + if (items == null) + return Result.Fail("Cloud items are null"); + var itemsList = items.ToList(); - var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + if (itemsList.Count == 0) + return Result.Fail("Cloud items are empty"); - using var graphics = Graphics.FromImage(bitmap); + if (canvasSettings.CanvasSize.Width <= 0 || + canvasSettings.CanvasSize.Height <= 0) + return Result.Fail("Invalid canvas size"); - ConfigureGraphics(graphics); - graphics.Clear(canvasSettings.BackgroundColor); + var cloudBounds = CalculateCloudBounds(itemsList); - var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); + if (!FitsCanvas(cloudBounds, canvasSettings.CanvasSize)) + return Result.Fail( + $"Tag cloud size ({cloudBounds.Width}x{cloudBounds.Height}) " + + $"exceeds canvas size ({canvasSettings.CanvasSize.Width}x{canvasSettings.CanvasSize.Height})"); + return Result.Of(() => + { + var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + using var graphics = Graphics.FromImage(bitmap); - using var brush = new SolidBrush(textSettings.TextColor); - using var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; - using var pen = new Pen(textSettings.TextColor, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; - foreach (var item in itemsList) - { - DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); - } + ConfigureGraphics(graphics); + graphics.Clear(canvasSettings.BackgroundColor); + + var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); - return bitmap; + using var brush = new SolidBrush(textSettings.TextColor); + using var stringFormat = new StringFormat { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center }; + using var pen = new Pen(textSettings.TextColor, 1) { DashStyle = System.Drawing.Drawing2D.DashStyle.Dash }; + + foreach (var item in itemsList) + DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); + return bitmap; + }, "Failed to render tag cloud image"); } private (int offsetX, int offsetY) CalculateOffset( @@ -90,5 +106,21 @@ private void ConfigureGraphics(Graphics graphics) graphics.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; graphics.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality; } + + private Rectangle CalculateCloudBounds(List items) + { + var minX = items.Min(i => i.Rectangle.Left); + var minY = items.Min(i => i.Rectangle.Top); + var maxX = items.Max(i => i.Rectangle.Right); + var maxY = items.Max(i => i.Rectangle.Bottom); + + return Rectangle.FromLTRB(minX, minY, maxX, maxY); + } + + private bool FitsCanvas(Rectangle cloudBounds, Size canvasSize) + { + return cloudBounds.Width <= canvasSize.Width + && cloudBounds.Height <= canvasSize.Height; + } } } diff --git a/TagCloudGenerator/Infrastructure/Result.cs b/TagCloudGenerator/Infrastructure/Result.cs new file mode 100644 index 00000000..39ca803c --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Result.cs @@ -0,0 +1,140 @@ +namespace TagCloudGenerator.Infrastructure +{ + public class None + { + private None() + { + } + } + + public struct Result + { + public Result(string error, T value = default) + { + Error = error; + Value = value; + } + public static implicit operator Result(T v) + { + return Result.Ok(v); + } + + public string Error { get; } + internal T Value { get; } + public T GetValueOrThrow() + { + if (IsSuccess) return Value; + throw new InvalidOperationException($"No value. Only Error {Error}"); + } + public bool IsSuccess => Error == null; + } + + public static class Result + { + public static Result AsResult(this T value) + { + return Ok(value); + } + + public static Result Ok(T value) + { + return new Result(null, value); + } + public static Result Ok() + { + return Ok(null); + } + + public static Result Fail(string e) + { + return new Result(e); + } + + public static Result Of(Func f, string error = null) + { + try + { + return Ok(f()); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result OfAction(Action f, string error = null) + { + try + { + f(); + return Ok(); + } + catch (Exception e) + { + return Fail(error ?? e.Message); + } + } + + public static Result Then( + this Result input, + Func continuation) + { + return input.Then(inp => Of(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Action continuation) + { + return input.Then(inp => OfAction(() => continuation(inp))); + } + + public static Result Then( + this Result input, + Func> continuation) + { + return input.IsSuccess + ? continuation(input.Value) + : Fail(input.Error); + } + + public static Result OnFail( + this Result input, + Action handleError) + { + if (!input.IsSuccess) handleError(input.Error); + return input; + } + + public static Result OnSuccess( + this Result result, + Action action) + { + if (result.IsSuccess) + action(result.Value); + return result; + } + + public static Result ReplaceError( + this Result input, + Func replaceError) + { + if (input.IsSuccess) return input; + return Fail(replaceError(input.Error)); + } + + public static Result RefineError( + this Result input, + string errorMessage) + { + return input.ReplaceError(err => errorMessage + ". " + err); + } + } +} diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs index 4a4155e4..4c062fdb 100644 --- a/TagCloudUIClient/MainForm.Designer.cs +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -1,6 +1,8 @@ using System.Drawing.Imaging; +using System.Drawing; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudUIClient @@ -13,7 +15,7 @@ public partial class MainForm : Form private System.ComponentModel.IContainer components = null; private readonly ITagCloudGenerator _generator; - private readonly IReaderRepository _readersRepository; + private readonly IReaderRepository _readerRepository; private readonly INormalizer _normalizer; private string _lastOutputPath = string.Empty; @@ -45,7 +47,7 @@ public partial class MainForm : Form public MainForm(ITagCloudGenerator generator, IReaderRepository readers, INormalizer normalizer) { _generator = generator; - _readersRepository = readers; + _readerRepository = readers; _normalizer = normalizer; InitializeComponent(); } @@ -64,26 +66,21 @@ private void btnChooseFile_Click(object sender, EventArgs e) private void LoadExcludedWords(string path) { clbExcludedWords.Items.Clear(); - try - { - if (_readersRepository.TryGetReader(path, out var outputReader)) - { - var words = outputReader.TryRead(path); - wordsToRender = _normalizer.Normalize(words); - } - else - { - throw new Exception("no suitable formate readers found"); - } - } - catch(Exception ex) - { - MessageBox.Show( - $"Ошибка чтения: {ex.Message}", - "Формат файла не поддерживается", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } + _readerRepository.TryGetReader(path) + .Then(reader => reader.TryRead(path)) + .Then(words => _normalizer.Normalize(words)) + .OnSuccess(words => + { + wordsToRender = words; + }) + .OnFail(error => { + MessageBox.Show( + $"{error}", + "Error", + MessageBoxButtons.OK, + MessageBoxIcon.Warning); + return; + }); var text = File.ReadAllText(path); var distinctWords = wordsToRender.Distinct().OrderBy(w => w); @@ -152,29 +149,21 @@ private void btnGenerate_Click(object sender, EventArgs e) .SetMinFontSize((int)numMinSize.Value) .SetTextColor(textColor); - try - { - var bitmap = _generator.Generate( - wordsToRender, - canvasSettings, - textSettings, - filters); - - picturePreview.Image?.Dispose(); - - _generatedBitmap = bitmap; - picturePreview.Image = bitmap; - - btnSave.Enabled = true; - } - catch (Exception ex) - { - MessageBox.Show( - $"Ошибка генерации: {ex.Message}", - "Генерация не удалась", + _generator.Generate(wordsToRender, canvasSettings, textSettings, filters) + .OnSuccess(bitmap => + { + picturePreview.Image?.Dispose(); + _generatedBitmap = bitmap; + picturePreview.Image = bitmap; + btnSave.Enabled = true; + }) + .OnFail(error => { + MessageBox.Show( + $"Generation error: {error}", + "Generation was not successful", MessageBoxButtons.OK, MessageBoxIcon.Error); - } + }); } @@ -364,7 +353,7 @@ private void InitializeComponent() numMaxSize.TabIndex = 7; numMaxSize.Value = new decimal(new int[] { 80, 0, 0, 0 }); numMaxSize.ValueChanged += numMaxSize_ValueChanged; - // + // // btnChooseFont // btnChooseFont.Location = new Point(10, 135); diff --git a/TagCloudUIClient/MainForm.cs b/TagCloudUIClient/MainForm.cs index e7d44043..42694581 100644 --- a/TagCloudUIClient/MainForm.cs +++ b/TagCloudUIClient/MainForm.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; @@ -27,4 +27,4 @@ private void numMaxSize_ValueChanged(object sender, EventArgs e) } } -} +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.resx b/TagCloudUIClient/MainForm.resx index 8b2ff64a..1af7de15 100644 --- a/TagCloudUIClient/MainForm.resx +++ b/TagCloudUIClient/MainForm.resx @@ -1,17 +1,17 @@  - diff --git a/TagCloudUIClient/TagCloudUIClient.csproj b/TagCloudUIClient/TagCloudUIClient.csproj index a1c082b4..f78d3be6 100644 --- a/TagCloudUIClient/TagCloudUIClient.csproj +++ b/TagCloudUIClient/TagCloudUIClient.csproj @@ -3,7 +3,7 @@ WinExe net8.0-windows - disable + enable true enable diff --git a/di.sln b/di.sln index 8553282a..533814f3 100644 --- a/di.sln +++ b/di.sln @@ -11,7 +11,7 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudGeneratorTests", "T EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudConsoleClient", "TagCloudConsoleClient\TagCloudConsoleClient.csproj", "{C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudUIClient", "TagCloudUIClient\TagCloudUIClient.csproj", "{6415D8CF-C1CB-4B75-AA1D-C0207B79020C}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TagCloudUIClient", "TagCloudUIClient\TagCloudUIClient.csproj", "{62B780CA-0CCB-4F52-B5BD-E294E53677BA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -35,10 +35,10 @@ Global {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Debug|Any CPU.Build.0 = Debug|Any CPU {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.ActiveCfg = Release|Any CPU {C8F88DBA-BFBC-4E4F-B305-E7EB928103AD}.Release|Any CPU.Build.0 = Release|Any CPU - {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6415D8CF-C1CB-4B75-AA1D-C0207B79020C}.Release|Any CPU.Build.0 = Release|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {62B780CA-0CCB-4F52-B5BD-E294E53677BA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 38a64daecbafc08466bccedbd13a920afc6f61ff Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 27 Dec 2025 06:21:39 +0500 Subject: [PATCH 8/8] Updated tests with new exceptions handling pattern --- TagCloudGeneratorTests/CloudGeneratorTests.cs | 13 +++-- TagCloudGeneratorTests/DocxReaderTests.cs | 15 +++--- .../ReaderRepositoryTests.cs | 14 ++--- TagCloudGeneratorTests/TxtReaderTests.cs | 51 +++++++++++-------- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index 9b7eafe5..402bab9d 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -39,7 +39,7 @@ public void Setup() } [Test] - public void Generate_EmptyWords_ReturnsNull_AndDoesNotRender_Test() + public void Generate_EmptyWords_ReturnsFailResult_AndDoesNotRender_Test() { filterMock .Setup(f => f.Filter(It.IsAny>())) @@ -51,7 +51,7 @@ public void Generate_EmptyWords_ReturnsNull_AndDoesNotRender_Test() new TextSettings(), new[] { filterMock.Object }); - Assert.IsNull(result); + Assert.That(result.IsSuccess, Is.False); rendererMock.Verify(r => r.Render( It.IsAny>(), @@ -61,7 +61,7 @@ public void Generate_EmptyWords_ReturnsNull_AndDoesNotRender_Test() } [Test] - public void Generate_AllWordsFilteredOut_ReturnsNull_Test() + public void Generate_AllWordsFilteredOut_ReturnsFailResult_Test() { var words = new List { "in", "a", "for" }; @@ -75,7 +75,7 @@ public void Generate_AllWordsFilteredOut_ReturnsNull_Test() new TextSettings(), new[] { filterMock.Object }); - Assert.IsNull(result); + Assert.That(result.IsSuccess, Is.False); rendererMock.Verify(r => r.Render( It.IsAny>(), @@ -143,9 +143,8 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() textSettings, new[] { filterMock.Object }); - Assert.IsNotNull(result); - - result.Dispose(); + Assert.That(result.IsSuccess, Is.True); + result.GetValueOrThrow().Dispose(); filterMock.Verify(f => f.Filter(words), Times.Once); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs index 3d20805d..0198c87a 100644 --- a/TagCloudGeneratorTests/DocxReaderTests.cs +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -1,6 +1,7 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using NUnit.Framework; +using TagCloudGenerator.Infrastructure; using TagCloudGenerator.Infrastructure.Readers; namespace TagCloudGeneratorTests @@ -41,7 +42,8 @@ public void TryRead_DocxWithParagraphs_ReturnsParagraphs_Test() var result = reader.TryRead(tempFilePath); - Assert.That(result, Is.EqualTo(new[] { "word1", "word2", "word3" })); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.EqualTo(new[] { "word1", "word2", "word3" })); } [Test] @@ -51,25 +53,26 @@ public void TryRead_EmptyDocx_ReturnsEmptyList_Test() var result = reader.TryRead(tempFilePath); - Assert.That(result, Is.Empty); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.Empty); } [Test] public void TryRead_DocxWithEmptyParagraphs_IgnoresThem_Test() { CreateDocx(new[] { "word1", "", " ", "word2" }); - var result = reader.TryRead(tempFilePath); - Assert.That(result, Is.EqualTo(new[] { "word1", "word2" })); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.EqualTo(new[] { "word1", "word2" })); } [Test] - public void TryRead_NonExistentFile_ReturnsEmptyList_Test() + public void TryRead_NonExistentFile_ReturnsFailResult_Test() { var result = reader.TryRead("nonexistent.docx"); - Assert.That(result, Is.Empty); + Assert.That(result.IsSuccess, Is.False); } private void CreateDocx(IEnumerable lines) diff --git a/TagCloudGeneratorTests/ReaderRepositoryTests.cs b/TagCloudGeneratorTests/ReaderRepositoryTests.cs index ec1761ae..e9c7f643 100644 --- a/TagCloudGeneratorTests/ReaderRepositoryTests.cs +++ b/TagCloudGeneratorTests/ReaderRepositoryTests.cs @@ -47,31 +47,27 @@ public void CanRead_WhenNoReaderCanRead_ReturnsFalse() public void TryRead_UsesFirstMatchingReader() { var expected = new List { "word1", "word2" }; - var result = new List(); firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); secondReaderMock.Setup(r => r.TryRead("file.docx")).Returns(expected); - if(readerRepository.TryGetReader("file.docx", out var reader)) - { - result = reader.TryRead("file.docx"); - } + var result = readerRepository.TryGetReader("file.docx"); + Assert.That(result.IsSuccess, Is.True); - Assert.That(result, Is.EqualTo(expected)); + Assert.That(result.GetValueOrThrow().TryRead("file.docx").GetValueOrThrow(), Is.EqualTo(expected)); secondReaderMock.Verify(r => r.TryRead("file.docx"), Times.Once); firstReaderMock.Verify(r => r.TryRead(It.IsAny()), Times.Never); } [Test] - public void TryRead_WhenNoReaderFound_ThrowsException() + public void TryRead_WhenNoReaderFound_ReturnsError() { firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); - Assert.That(readerRepository.TryGetReader("file.xyz", out var reader), Is.False); - Assert.That(reader, Is.Null); + Assert.That(readerRepository.TryGetReader("file.xyz").IsSuccess, Is.False); } } } diff --git a/TagCloudGeneratorTests/TxtReaderTests.cs b/TagCloudGeneratorTests/TxtReaderTests.cs index e9116c8e..d9d8825f 100644 --- a/TagCloudGeneratorTests/TxtReaderTests.cs +++ b/TagCloudGeneratorTests/TxtReaderTests.cs @@ -21,8 +21,9 @@ public void TryRead_ExistingFile_ReturnsLines_Test() var expectedLines = new[] { "line1", "line2", "line3" }; WriteToTempFile(string.Join(Environment.NewLine, expectedLines)); - var result = reader.TryRead(tempFilePath).ToList(); - Assert.That(result, Is.EqualTo(expectedLines)); + var result = reader.TryRead(tempFilePath); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().ToList(), Is.EqualTo(expectedLines)); } [Test] @@ -30,30 +31,35 @@ public void TryRead_EmptyFile_ReturnsEmpty_Test() { WriteToTempFile(""); var result = reader.TryRead(tempFilePath); - Assert.That(result, Is.Empty); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow(), Is.Empty); } [Test] public void TryRead_FileWithWhitespaceLines_ReturnsAllLines_Test() { WriteToTempFile("line1\n\nline2\n \nline3"); - var result = reader.TryRead(tempFilePath).ToList(); - - Assert.That(result.Count, Is.EqualTo(5)); - Assert.That(result[0], Is.EqualTo("line1")); - Assert.That(result[1], Is.EqualTo("")); - Assert.That(result[2], Is.EqualTo("line2")); - Assert.That(result[3], Is.EqualTo(" ")); - Assert.That(result[4], Is.EqualTo("line3")); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); + var lines = result.GetValueOrThrow().ToList(); + + Assert.That(lines.Count, Is.EqualTo(5)); + Assert.That(lines[0], Is.EqualTo("line1")); + Assert.That(lines[1], Is.EqualTo("")); + Assert.That(lines[2], Is.EqualTo("line2")); + Assert.That(lines[3], Is.EqualTo(" ")); + Assert.That(lines[4], Is.EqualTo("line3")); } [Test] public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines_Test() { WriteToTempFile("line1\r\nline2\r\nline3"); - var result = reader.TryRead(tempFilePath).ToList(); + var result = reader.TryRead(tempFilePath); - Assert.That(result, Is.EqualTo(new[] { "line1", "line2", "line3" })); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().ToList(), Is.EqualTo(new[] { "line1", "line2", "line3" })); } [Test] @@ -61,27 +67,32 @@ public void TryRead_NonExistentFile_ReturnsNull_Test() { var nonExistentPath = "C:\\nonexistent\\file.txt"; var result = reader.TryRead(nonExistentPath); - Assert.That(result, Is.Null); + Assert.That(result.IsSuccess, Is.False); } [Test] public void TryRead_FileWithOneLine_ReturnsOneLine_Test() { WriteToTempFile("single line"); - var result = reader.TryRead(tempFilePath).ToList(); + var result = reader.TryRead(tempFilePath); - Assert.That(result.Count, Is.EqualTo(1)); - Assert.That(result[0], Is.EqualTo("single line")); + Assert.That(result.IsSuccess, Is.True); + Assert.That(result.GetValueOrThrow().Count, Is.EqualTo(1)); + Assert.That(result.GetValueOrThrow()[0], Is.EqualTo("single line")); } [Test] public void TryRead_FileWithTrailingNewline_ReturnsCorrectLines_Test() { WriteToTempFile("line1\nline2\n"); - var result = reader.TryRead(tempFilePath).ToList(); + var result = reader.TryRead(tempFilePath); + + Assert.That(result.IsSuccess, Is.True); - Assert.That(result.Count, Is.EqualTo(2)); - Assert.That(result, Is.EqualTo(new[] { "line1", "line2" })); + var lines = result.GetValueOrThrow(); + Assert.That(lines.Count, Is.EqualTo(2)); + Assert.That(lines[0], Is.EqualTo("line1")); + Assert.That(lines[1], Is.EqualTo("line2")); } private void WriteToTempFile(string content)