From eed1d8a251c14d37890704d5221dc9ddd20ebeb8 Mon Sep 17 00:00:00 2001 From: Xineev Date: Mon, 29 Dec 2025 19:36:56 +0300 Subject: [PATCH 01/13] Import solution from previous task --- TagCloudConsoleClient/ConsoleClient.cs | 104 ++++ TagCloudConsoleClient/Options.cs | 41 ++ TagCloudConsoleClient/Program.cs | 25 + .../TagCloudConsoleClient.csproj | 14 + .../Algorithms/BasicTagCloudAlgorithm.cs | 100 ++++ .../Core/Interfaces/IAnalyzer.cs | 9 + TagCloudGenerator/Core/Interfaces/IClient.cs | 7 + TagCloudGenerator/Core/Interfaces/IFilter.cs | 8 + .../Core/Interfaces/IFontSizeCalculator.cs | 8 + .../Core/Interfaces/IFormatReader.cs | 10 + .../Core/Interfaces/INormalizer.cs | 7 + .../Core/Interfaces/IReaderRepository.cs | 9 + .../Core/Interfaces/IRenderer.cs | 11 + .../Core/Interfaces/ISorterer.cs | 8 + .../Core/Interfaces/ITagCloudAlgorithm.cs | 12 + .../Core/Interfaces/ITagCloudGenerator.cs | 11 + .../Core/Interfaces/ITextMeasurer.cs | 9 + .../Core/Models/CanvasSettings.cs | 62 +++ TagCloudGenerator/Core/Models/CloudItem.cs | 49 ++ TagCloudGenerator/Core/Models/TextSettings.cs | 43 ++ .../Core/Services/CloudGenerator.cs | 96 ++++ TagCloudGenerator/DI/TagCloudModule.cs | 47 ++ .../Analyzers/WordsFrequencyAnalyzer.cs | 26 + .../Calculators/LinearFontSizeCalculator.cs | 16 + .../Filters/BoringWordsFilter.cs | 28 + .../Measurers/GraphicsTextMeasurer.cs | 18 + .../Normalizers/LowerCaseNormalizer.cs | 14 + .../Infrastructure/Readers/DocxReader.cs | 32 ++ .../Readers/ReaderRepository.cs | 28 + .../Infrastructure/Readers/TxtReader.cs | 25 + .../Infrastructure/Renderers/PngRenderer.cs | 105 ++++ TagCloudGenerator/Infrastructure/Result.cs | 140 +++++ .../Sorterers/FrequencyDescendingSorterer.cs | 16 + TagCloudGenerator/TagCloudGenerator.csproj | 17 + .../BoringWordsFilterTests.cs | 82 +++ TagCloudGeneratorTests/CloudGeneratorTests.cs | 165 ++++++ TagCloudGeneratorTests/DocxReaderTests.cs | 94 ++++ .../GraphicsTextMeasurerTests.cs | 77 +++ .../LinearFontSizeCalculatorTests.cs | 71 +++ .../ReaderRepositoryTests.cs | 77 +++ .../TagCloudGeneratorTests.csproj | 25 + TagCloudGeneratorTests/TxtReaderTests.cs | 92 ++++ .../WordsFrequencyAnalyzerTests.cs | 52 ++ TagCloudUIClient/MainForm.Designer.cs | 499 ++++++++++++++++++ TagCloudUIClient/MainForm.cs | 30 ++ TagCloudUIClient/MainForm.resx | 120 +++++ TagCloudUIClient/Program.cs | 29 + TagCloudUIClient/TagCloudUIClient.csproj | 15 + TagCloudUIClient/WinFormsClient.cs | 27 + fp.sln | 38 +- 50 files changed, 2644 insertions(+), 4 deletions(-) create mode 100644 TagCloudConsoleClient/ConsoleClient.cs create mode 100644 TagCloudConsoleClient/Options.cs create mode 100644 TagCloudConsoleClient/Program.cs create mode 100644 TagCloudConsoleClient/TagCloudConsoleClient.csproj create mode 100644 TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IAnalyzer.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IClient.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFilter.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IFormatReader.cs create mode 100644 TagCloudGenerator/Core/Interfaces/INormalizer.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IReaderRepository.cs create mode 100644 TagCloudGenerator/Core/Interfaces/IRenderer.cs create mode 100644 TagCloudGenerator/Core/Interfaces/ISorterer.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/CanvasSettings.cs create mode 100644 TagCloudGenerator/Core/Models/CloudItem.cs create mode 100644 TagCloudGenerator/Core/Models/TextSettings.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/Normalizers/LowerCaseNormalizer.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/DocxReader.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs create mode 100644 TagCloudGenerator/Infrastructure/Readers/TxtReader.cs create mode 100644 TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs create mode 100644 TagCloudGenerator/Infrastructure/Result.cs create mode 100644 TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs create mode 100644 TagCloudGenerator/TagCloudGenerator.csproj create mode 100644 TagCloudGeneratorTests/BoringWordsFilterTests.cs create mode 100644 TagCloudGeneratorTests/CloudGeneratorTests.cs create mode 100644 TagCloudGeneratorTests/DocxReaderTests.cs create mode 100644 TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs create mode 100644 TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs create mode 100644 TagCloudGeneratorTests/ReaderRepositoryTests.cs create mode 100644 TagCloudGeneratorTests/TagCloudGeneratorTests.csproj create mode 100644 TagCloudGeneratorTests/TxtReaderTests.cs create mode 100644 TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs 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/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs new file mode 100644 index 000000000..1aef52eb4 --- /dev/null +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -0,0 +1,104 @@ +using CommandLine; +using System.Drawing; +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Readers; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace TagCloudConsoleClient +{ + public class ConsoleClient : IClient + { + private readonly ITagCloudGenerator _generator; + private readonly IEnumerable _filters; + private readonly IReaderRepository _readersRepository; + private readonly INormalizer _normalizer; + + public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReaderRepository readers, INormalizer normalizer) + { + _generator = generator; + _filters = filters; + _readersRepository = readers; + _normalizer = normalizer; + } + + public void Run(string[] args) + { + Parser.Default.ParseArguments(args) + .WithParsed(RunWithOptions) + .WithNotParsed(errors => { }); + } + + private void RunWithOptions(Options opts) + { + if (!File.Exists(opts.InputFile)) + { + Console.WriteLine($"Error: input file '{opts.InputFile}' does not exist."); + return; + } + + var canvasSettings = new CanvasSettings() + .SetSize(opts.Width, opts.Height) + .SetBackgroundColor(TryParseColor(opts.BackgroundColor)) + .WithShowRectangles() + .SetPadding(opts.Padding); + + var textSettings = new TextSettings() + .SetFontFamily(opts.FontFamily) + .SetFontSizeRange(opts.MinFontSize, opts.MaxFontSize) + .SetTextColor(TryParseColor(opts.TextColor)); + + string inputFile = opts.InputFile; + string outputFile = opts.OutputFile; + + Console.WriteLine("Starting tag cloud generation..."); + 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 + { + throw new Exception("no suitable formate readers found"); + } + } + catch (Exception ex) + { + Console.WriteLine($"Error during generation: {ex.Message}"); + } + } + + private static Color? TryParseColor(string colorStr) + { + if (string.IsNullOrWhiteSpace(colorStr)) + return null; + + try + { + var known = Color.FromName(colorStr); + if (known.IsKnownColor) + return known; + + return ColorTranslator.FromHtml(colorStr); + } + catch + { + Console.WriteLine($"Color '{colorStr}' could not be parsed. Default color will be used."); + return null; + } + } + } +} diff --git a/TagCloudConsoleClient/Options.cs b/TagCloudConsoleClient/Options.cs new file mode 100644 index 000000000..2c5220cf9 --- /dev/null +++ b/TagCloudConsoleClient/Options.cs @@ -0,0 +1,41 @@ +using CommandLine; + +namespace TagCloudConsoleClient +{ + 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; } + } +} diff --git a/TagCloudConsoleClient/Program.cs b/TagCloudConsoleClient/Program.cs new file mode 100644 index 000000000..4944c8f91 --- /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 000000000..f4c4f3d1b --- /dev/null +++ b/TagCloudConsoleClient/TagCloudConsoleClient.csproj @@ -0,0 +1,14 @@ + + + + Exe + net8.0-windows + enable + disable + + + + + + + diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs new file mode 100644 index 000000000..403a5bf13 --- /dev/null +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -0,0 +1,100 @@ +using System.Drawing; +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; + } + + public ITagCloudAlgorithm Reset() + { + rectangles.Clear(); + return this; + } + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs new file mode 100644 index 000000000..8c47d59dd --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -0,0 +1,9 @@ +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IAnalyzer + { + Dictionary Analyze(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs new file mode 100644 index 000000000..de0e1098c --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -0,0 +1,7 @@ +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IClient + { + void Run(string[] args); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFilter.cs b/TagCloudGenerator/Core/Interfaces/IFilter.cs new file mode 100644 index 000000000..e660e7827 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFilter.cs @@ -0,0 +1,8 @@ + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IFilter + { + List Filter(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs new file mode 100644 index 000000000..57033c7e1 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFontSizeCalculator.cs @@ -0,0 +1,8 @@ + +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/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs new file mode 100644 index 000000000..a39bbe17a --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -0,0 +1,10 @@ +using TagCloudGenerator.Infrastructure; + +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 000000000..daaaf97bc --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -0,0 +1,7 @@ +namespace TagCloudGenerator.Core.Interfaces +{ + public interface INormalizer + { + public List Normalize(List words); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs new file mode 100644 index 000000000..9a96b90b7 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -0,0 +1,9 @@ +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface IReaderRepository + { + public bool TryGetReader(string filePath, out IFormatReader outputReader); + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Core/Interfaces/IRenderer.cs b/TagCloudGenerator/Core/Interfaces/IRenderer.cs new file mode 100644 index 000000000..3c60b7f5f --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -0,0 +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); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ISorterer.cs b/TagCloudGenerator/Core/Interfaces/ISorterer.cs new file mode 100644 index 000000000..b504e89d6 --- /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 new file mode 100644 index 000000000..1331e9b86 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -0,0 +1,12 @@ +using System.Drawing; +using TagCloudGenerator.Algorithms; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITagCloudAlgorithm + { + Rectangle PutNextRectangle(Size rectangleSize); + + public ITagCloudAlgorithm Reset(); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs new file mode 100644 index 000000000..23b996aff --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -0,0 +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); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs new file mode 100644 index 000000000..0f1f73be9 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs @@ -0,0 +1,9 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ITextMeasurer + { + Size Measure(string word, float fontSize, string fontFamily); + } +} diff --git a/TagCloudGenerator/Core/Models/CanvasSettings.cs b/TagCloudGenerator/Core/Models/CanvasSettings.cs new file mode 100644 index 000000000..2f4beafa8 --- /dev/null +++ b/TagCloudGenerator/Core/Models/CanvasSettings.cs @@ -0,0 +1,62 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class CanvasSettings + { + 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) + { + var size = CanvasSize; + + size.Width = width is > 0 ? width.Value : CanvasSize.Width; + CanvasSize = size; + + return this; + } + + public CanvasSettings SetHeight(int? height) + { + var size = CanvasSize; + + size.Height = height is > 0 ? height.Value : CanvasSize.Height; + CanvasSize = size; + + return this; + } + + public CanvasSettings SetSize(int? width, int? height) + { + 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; + } + + public CanvasSettings SetBackgroundColor(Color? color) + { + if (color.HasValue) + BackgroundColor = color.Value; + return this; + } + + public CanvasSettings WithShowRectangles(bool value = true) + { + ShowRectangles = value; + return this; + } + + public CanvasSettings SetPadding(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 new file mode 100644 index 000000000..c6a00eeac --- /dev/null +++ b/TagCloudGenerator/Core/Models/CloudItem.cs @@ -0,0 +1,49 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class CloudItem + { + public string Word { get; } + public Rectangle Rectangle { get; } + public float FontSize { get; } + public Color? TextColor { get; } + public string FontFamily { get; } + public FontStyle FontStyle { get; } + public int Frequency { get; } + + public CloudItem( + string word, + Rectangle rectangle, + float fontSize, + Color? textColor = 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; + TextColor = textColor; + FontFamily = fontFamily ?? throw new ArgumentNullException(nameof(fontFamily)); + FontStyle = fontStyle; + Frequency = frequency; + } + + public CloudItem WithRectangle(Rectangle newRectangle) + { + return new CloudItem(Word, newRectangle, FontSize, TextColor, FontFamily, FontStyle, Frequency); + } + + public CloudItem WithFontSize(float newFontSize) + { + 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); + } + } +} diff --git a/TagCloudGenerator/Core/Models/TextSettings.cs b/TagCloudGenerator/Core/Models/TextSettings.cs new file mode 100644 index 000000000..a7ca6145d --- /dev/null +++ b/TagCloudGenerator/Core/Models/TextSettings.cs @@ -0,0 +1,43 @@ +using System.Drawing; + +namespace TagCloudGenerator.Core.Models +{ + public class TextSettings + { + 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; + return this; + } + + public TextSettings SetMinFontSize(float? size) + { + MinFontSize = size is >= 0 ? size.Value : MinFontSize; + return this; + } + + public TextSettings SetMaxFontSize(float? size) + { + MaxFontSize = size is >= 0 ? size.Value : MaxFontSize; + 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 new file mode 100644 index 000000000..34e2f363b --- /dev/null +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -0,0 +1,96 @@ +using System.Drawing; +using TagCloudGenerator.Algorithms; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudGenerator.Core.Services +{ + public class CloudGenerator : ITagCloudGenerator + { + private readonly ITagCloudAlgorithm _algorithm; + private readonly IAnalyzer _analyzer; + 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, + ISorterer sorterer) + { + this._algorithm = algorithm; + _analyzer = analyzer; + _renderer = renderer; + _fontSizeCalculator = fontSizeCalculator; + _textMeasurer = textMeasurer; + _sorterer = sorterer; + _center = new Point(0, 0); + } + + public Bitmap? Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) + { + words = ApplyFilters(words, filters); + if (words.Count == 0) return null; + + var wordsWithFreq = _sorterer.Sort(_analyzer.Analyze(words)); + + var initializedItems = InitializeCloudItems(wordsWithFreq, textSettings).ToList(); + + _algorithm.Reset(); + + return _renderer.Render(initializedItems, canvasSettings, textSettings); + } + + private List ApplyFilters(List words, IEnumerable filters) + { + var filteredWords = words; + foreach (var filter in filters) + { + filteredWords = filter.Filter(filteredWords); + } + return filteredWords; + } + + private IEnumerable InitializeCloudItems(IEnumerable<(string Word, int Frequency)> items, TextSettings settings) + { + var itemsList = new List(); + + 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, + maxFrequency, + settings.MinFontSize, + settings.MaxFontSize); + + var textSize = _textMeasurer.Measure( + item.Word, + fontSize, + settings.FontFamily); + + var itemRectangle = _algorithm.PutNextRectangle(textSize); + + itemsList.Add( + new CloudItem( + word: item.Word, + rectangle: itemRectangle, + fontSize: fontSize, + textColor: settings.TextColor, + fontFamily: settings.FontFamily, + frequency: item.Frequency + )); + } + return itemsList; + } + } +} diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs new file mode 100644 index 000000000..3df3fe708 --- /dev/null +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -0,0 +1,47 @@ +using Autofac; +using TagCloudGenerator.Algorithms; +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.Normalizers; +using TagCloudGenerator.Infrastructure.Readers; +using TagCloudGenerator.Infrastructure.Renderers; +using TagCloudGenerator.Infrastructure.Sorterers; + +namespace TagCloudGenerator.DI +{ + public class TagCloudModule : Autofac.Module + { + protected override void Load(ContainerBuilder builder) + { + builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType() + .As() + .SingleInstance(); + + builder.RegisterType().As().SingleInstance(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + + 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 000000000..f63083826 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -0,0 +1,26 @@ +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Analyzers +{ + public class WordsFrequencyAnalyzer : IAnalyzer + { + public Dictionary Analyze(List words) + { + var wordFreqDictionary = new Dictionary(); + + if (words == null || words.Count == 0) + return new Dictionary(); + + foreach (string word in words) + { + if(wordFreqDictionary.TryAdd(word, 1)) + continue; + else wordFreqDictionary[word]++; + } + + return wordFreqDictionary; + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs new file mode 100644 index 000000000..8fc0ed54e --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Calculators/LinearFontSizeCalculator.cs @@ -0,0 +1,16 @@ +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 000000000..4cfdbd033 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -0,0 +1,28 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Filters +{ + public class BoringWordsFilter : IFilter + { + private string[] boringWords = ["in", "it", "a", "as", "for", "of", "on"]; + + public BoringWordsFilter() { } + + public BoringWordsFilter(IEnumerable words) + { + boringWords = words.ToArray(); + } + + public List Filter(List words) + { + 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); + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs new file mode 100644 index 000000000..5ecdd40c0 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs @@ -0,0 +1,18 @@ +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Measurers +{ + public class GraphicsTextMeasurer : ITextMeasurer + { + 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/Normalizers/LowerCaseNormalizer.cs b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs new file mode 100644 index 000000000..83ef709bb --- /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/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs new file mode 100644 index 000000000..8126d4c94 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -0,0 +1,32 @@ +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/ReaderRepository.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs new file mode 100644 index 000000000..dfc8e973d --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -0,0 +1,28 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class ReaderRepository : IReaderRepository + { + private readonly IEnumerable _readers; + + public ReaderRepository(IEnumerable readers) + { + _readers = readers; + } + + public bool CanRead(string filePath) + { + return _readers.Any(r => r.CanRead(filePath)); + } + + public bool TryGetReader(string filePath, out IFormatReader outputReader) + { + outputReader = _readers.FirstOrDefault(r => r.CanRead(filePath)); + if (outputReader == null) + return false; + + return true; + } + } +} \ No newline at end of file diff --git a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs new file mode 100644 index 000000000..a14801572 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs @@ -0,0 +1,25 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Readers +{ + public class TxtReader : IFormatReader + { + public bool CanRead(string filePath) + { + return Path.GetExtension(filePath).Equals(".txt", StringComparison.OrdinalIgnoreCase); + } + + public List TryRead(string filePath) + { + try + { + return File.ReadAllLines(filePath).ToList(); + } + 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 000000000..cd33762a2 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -0,0 +1,105 @@ +using System.Drawing; +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Renderers +{ + public class PngRenderer : IRenderer + { + public Bitmap Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) + { + var itemsList = items.ToList(); + var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); + using var graphics = Graphics.FromImage(bitmap); + ConfigureGraphics(graphics); + graphics.Clear(canvasSettings.BackgroundColor); + 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, canvasSettings, textSettings, brush, pen, stringFormat); + } + + return bitmap; + } + + private (int offsetX, int offsetY) CalculateOffset( + List items, + CanvasSettings settings) + { + 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, + CanvasSettings canvasSettings, + TextSettings textSettings, + SolidBrush brush, + Pen pen, + StringFormat stringFormat) + { + var drawRect = new Rectangle( + item.Rectangle.X + offsetX, + item.Rectangle.Y + offsetY, + item.Rectangle.Width, + item.Rectangle.Height); + + var color = item.TextColor ?? textSettings.TextColor; + + using var font = new Font( + item.FontFamily, + item.FontSize, + item.FontStyle, + GraphicsUnit.Pixel); + + graphics.DrawString(item.Word, font, brush, drawRect, stringFormat); + + if (canvasSettings.ShowRectangles) + { + 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; + } + + 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 000000000..39ca803c5 --- /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/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs new file mode 100644 index 000000000..0bdc20c3e --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs @@ -0,0 +1,16 @@ +using TagCloudGenerator.Core.Interfaces; + +namespace TagCloudGenerator.Infrastructure.Sorterers +{ + 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/TagCloudGenerator.csproj b/TagCloudGenerator/TagCloudGenerator.csproj new file mode 100644 index 000000000..3937f9c93 --- /dev/null +++ b/TagCloudGenerator/TagCloudGenerator.csproj @@ -0,0 +1,17 @@ + + + + Library + net8.0-windows + enable + disable + + + + + + + + + + diff --git a/TagCloudGeneratorTests/BoringWordsFilterTests.cs b/TagCloudGeneratorTests/BoringWordsFilterTests.cs new file mode 100644 index 000000000..52232f6ab --- /dev/null +++ b/TagCloudGeneratorTests/BoringWordsFilterTests.cs @@ -0,0 +1,82 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Filters; + +namespace TagCloudGeneratorTests +{ + public class BoringWordsFilterTests + { + private BoringWordsFilter filter = new BoringWordsFilter(); + + [Test] + public void Filter_EmptyInput_ReturnsEmpty() + { + var words = new List(); + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_RemovesBoringWords_Test() + { + var words = new List { "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_Test() + { + var words = new List { "in", "a", "for", "on" }; + var result = filter.Filter(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Filter_NoBoringWords_ReturnsAllWords_Test() + { + var words = new List { "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_Test() + { + var boringWords = new List { "in", "it", "a", "as", "for", "of", "on" }; + foreach (var word in boringWords) + { + Assert.That(filter.ShouldInclude(word), Is.False); + } + } + + [Test] + public void ShouldInclude_ReturnsTrueForNormalWords_Test() + { + var normalWords = new List { "hello", "world", "computer", "programming", "test" }; + foreach (var word in normalWords) + { + Assert.That(filter.ShouldInclude(word), Is.True); + } + } + + [Test] + public void Filter_PreservesOrder_Test() + { + var words = new List { "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 000000000..e380fab23 --- /dev/null +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -0,0 +1,165 @@ +using Moq; +using NUnit.Framework; +using System.Drawing; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Services; + +namespace TagCloudGeneratorTests +{ + public class CloudGeneratorTests + { + private Mock algorithmMock; + private Mock filterMock; + private Mock analyzerMock; + private Mock rendererMock; + private Mock fontSizeCalculatorMock; + private Mock textMeasurerMock; + private Mock sortererMock; + private CloudGenerator cloudGenerator; + + [SetUp] + public void Setup() + { + algorithmMock = new Mock(); + filterMock = new Mock(); + analyzerMock = new Mock(); + 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, + sortererMock.Object); + } + + [Test] + public void Generate_EmptyWords_ReturnsNull_AndDoesNotRender_Test() + { + filterMock + .Setup(f => f.Filter(It.IsAny>())) + .Returns(new List()); + + 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()), + Times.Never); + } + + [Test] + public void Generate_AllWordsFilteredOut_ReturnsNull_Test() + { + var words = new List { "in", "a", "for" }; + + 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()), + Times.Never); + } + + [Test] + public void Generate_NormalFlow_CallsAllDependencies_Test() + { + var words = new List { "hello", "world", "hello" }; + var filteredWords = new List { "hello", "world", "hello" }; + + 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)) + .Returns(filteredWords); + + analyzerMock + .Setup(a => a.Analyze(filteredWords)) + .Returns(analyzed); + + sortererMock + .Setup(s => s.Sort(analyzed)) + .Returns(sorted); + + 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); + 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); + algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); + + rendererMock.Verify(r => r.Render( + It.Is>(items => items.Count() == 2), + It.IsAny(), + It.Is(ts => + ts.FontFamily == "Arial" && + Math.Abs(ts.MinFontSize - 12) < float.Epsilon && + Math.Abs(ts.MaxFontSize - 72) < float.Epsilon)), + Times.Once); + } + + } +} diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs new file mode 100644 index 000000000..ec47fda1d --- /dev/null +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -0,0 +1,94 @@ +using DocumentFormat.OpenXml.Packaging; +using DocumentFormat.OpenXml.Wordprocessing; +using NUnit.Framework; +using TagCloudGenerator.Infrastructure; +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/GraphicsTextMeasurerTests.cs b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs new file mode 100644 index 000000000..ecfa6b634 --- /dev/null +++ b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Measurers; + +namespace TagCloudGeneratorTests +{ + public class GraphicsTextMeasurerTests + { + private GraphicsTextMeasurer measurer = new GraphicsTextMeasurer(); + + [Test] + public void Measure_EmptyString_ReturnsValidSize_Test() + { + 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_Test() + { + 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_Test() + { + 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_Test() + { + 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_Test() + { + 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_Test() + { + 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_Test() + { + 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_Test() + { + 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/LinearFontSizeCalculatorTests.cs b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs new file mode 100644 index 000000000..b9fe20df3 --- /dev/null +++ b/TagCloudGeneratorTests/LinearFontSizeCalculatorTests.cs @@ -0,0 +1,71 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Calculators; + +namespace TagCloudGeneratorTests +{ + public class LinearFontSizeCalculatorTests + { + private LinearFontSizeCalculator calculator = new LinearFontSizeCalculator(); + + [Test] + 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_Test() + { + var result = calculator.Calculate(10, 1, 10, 12f, 72f); + Assert.That(result, Is.EqualTo(72f)); + } + + [Test] + 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_Test() + { + var result = calculator.Calculate(5, 5, 5, 12f, 72f); + Assert.That(result, Is.EqualTo(42f)); + } + + [Test] + 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_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_Test() + { + 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_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/ReaderRepositoryTests.cs b/TagCloudGeneratorTests/ReaderRepositoryTests.cs new file mode 100644 index 000000000..52d956545 --- /dev/null +++ b/TagCloudGeneratorTests/ReaderRepositoryTests.cs @@ -0,0 +1,77 @@ +using Moq; +using NUnit.Framework; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class ReaderRepositoryTests + { + private Mock firstReaderMock; + private Mock secondReaderMock; + private ReaderRepository readerRepository; + + [SetUp] + public void Setup() + { + firstReaderMock = new Mock(); + secondReaderMock = new Mock(); + + readerRepository = new ReaderRepository( + new[] { firstReaderMock.Object, secondReaderMock.Object }); + } + + [Test] + public void CanRead_WhenAnyReaderCanRead_ReturnsTrue() + { + firstReaderMock.Setup(r => r.CanRead("file.docx")).Returns(false); + secondReaderMock.Setup(r => r.CanRead("file.docx")).Returns(true); + + var result = readerRepository.CanRead("file.docx"); + + Assert.That(result, Is.True); + } + + [Test] + public void CanRead_WhenNoReaderCanRead_ReturnsFalse() + { + firstReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + secondReaderMock.Setup(r => r.CanRead(It.IsAny())).Returns(false); + + var result = readerRepository.CanRead("file.unknown"); + + Assert.That(result, Is.False); + } + + [Test] + 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"); + } + + Assert.That(result, 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() + { + 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); + } + } +} diff --git a/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj new file mode 100644 index 000000000..94cb41bc1 --- /dev/null +++ b/TagCloudGeneratorTests/TagCloudGeneratorTests.csproj @@ -0,0 +1,25 @@ + + + + net8.0-windows + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/TagCloudGeneratorTests/TxtReaderTests.cs b/TagCloudGeneratorTests/TxtReaderTests.cs new file mode 100644 index 000000000..e9116c8e7 --- /dev/null +++ b/TagCloudGeneratorTests/TxtReaderTests.cs @@ -0,0 +1,92 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudGeneratorTests +{ + public class TxtReaderTests + { + private TxtReader reader = new TxtReader(); + private string tempFilePath = Path.GetTempFileName(); + + [TearDown] + public void TearDown() + { + if (File.Exists(tempFilePath)) + File.Delete(tempFilePath); + } + + [Test] + 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)); + } + + [Test] + public void TryRead_EmptyFile_ReturnsEmpty_Test() + { + WriteToTempFile(""); + var result = reader.TryRead(tempFilePath); + Assert.That(result, 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")); + } + + [Test] + public void TryRead_FileWithWindowsLineEndings_ReturnsCorrectLines_Test() + { + 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_Test() + { + var nonExistentPath = "C:\\nonexistent\\file.txt"; + var result = reader.TryRead(nonExistentPath); + Assert.That(result, Is.Null); + } + + [Test] + public void TryRead_FileWithOneLine_ReturnsOneLine_Test() + { + 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_Test() + { + 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" })); + } + + private void WriteToTempFile(string content) + { + File.WriteAllText(tempFilePath, content); + } + } +} diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs new file mode 100644 index 000000000..f5eb2c556 --- /dev/null +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -0,0 +1,52 @@ +using NUnit.Framework; +using TagCloudGenerator.Infrastructure.Analyzers; + +namespace TagCloudGeneratorTests +{ + public class WordsFrequencyAnalyzerTests + { + private WordsFrequencyAnalyzer frequencyAnalyzer = new WordsFrequencyAnalyzer(); + + [Test] + public void Analyze_EmptyInput_ReturnsEmpty_Test() + { + var words = new List(); + var result = frequencyAnalyzer.Analyze(words); + Assert.That(result, Is.Empty); + } + + [Test] + public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() + { + var words = new List { "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].Key, Is.EqualTo("hello")); + Assert.That(result[0].Value, Is.EqualTo(1)); + } + + [Test] + public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() + { + var words = new List { "hello", "world", "hello", "test", "world", "hello" }; + var result = frequencyAnalyzer.Analyze(words).ToList(); + + Assert.That(result.Count, Is.EqualTo(3)); + 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] + public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() + { + var words = new List { "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/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs new file mode 100644 index 000000000..488649b77 --- /dev/null +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -0,0 +1,499 @@ +using System.Drawing; +using System.Drawing.Imaging; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Infrastructure.Filters; +using TagCloudGenerator.Infrastructure.Readers; + +namespace TagCloudUIClient +{ + public partial class MainForm : Form + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + private readonly ITagCloudGenerator _generator; + private readonly IReaderRepository _readersRepository; + 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, IReaderRepository readers, INormalizer normalizer) + { + _generator = generator; + _readersRepository = readers; + _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(); + 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); + } + + 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 000000000..426945814 --- /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) + { + + } + } +} \ No newline at end of file diff --git a/TagCloudUIClient/MainForm.resx b/TagCloudUIClient/MainForm.resx new file mode 100644 index 000000000..1af7de150 --- /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 000000000..650b4211a --- /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 000000000..f78d3be68 --- /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 000000000..c0ffe1b54 --- /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 IReaderRepository _reader; + private readonly INormalizer _normalizer; + + public WinFormsClient(ITagCloudGenerator generator, IReaderRepository repository, INormalizer normalizer) + { + _generator = generator; + _reader = repository; + _normalizer = normalizer; + } + + public void Run(string[] args) + { + Application.EnableVisualStyles(); + Application.SetCompatibleTextRenderingDefault(false); + + var mainForm = new MainForm(_generator, _reader, _normalizer); + Application.Run(mainForm); + } + } +} diff --git a/fp.sln b/fp.sln index d104ab530..40b90a03c 100644 --- a/fp.sln +++ b/fp.sln @@ -1,14 +1,25 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileSenderRailway", "FileSenderRailway\FileSenderRailway.csproj", "{D979A1EA-516A-46BC-BE6C-8845CA10853D}" +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FileSenderRailway", "FileSenderRailway\FileSenderRailway.csproj", "{D979A1EA-516A-46BC-BE6C-8845CA10853D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ErrorHandling", "ErrorHandling\ErrorHandling.csproj", "{66FAF276-533D-4733-AB2E-A9905D678CF6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ErrorHandling", "ErrorHandling\ErrorHandling.csproj", "{66FAF276-533D-4733-AB2E-A9905D678CF6}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{754C1CC8-A8B6-46C6-B35C-8A43B80111A0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Summator", "Samples\Summator\Summator.csproj", "{C33F3A5E-A1ED-4657-9B35-968A4CB23AA1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Summator", "Samples\Summator\Summator.csproj", "{C33F3A5E-A1ED-4657-9B35-968A4CB23AA1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConwaysGameOfLife", "Samples\ConwaysGameOfLife\ConwaysGameOfLife.csproj", "{4B77EC28-5FB5-4095-B3D7-127F5C488D6E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudGenerator", "TagCloudGenerator\TagCloudGenerator.csproj", "{C824111A-FF7E-42C9-A4F3-A912E6BD39E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudConsoleClient", "TagCloudConsoleClient\TagCloudConsoleClient.csproj", "{2C8600EB-F62C-42EB-9EAA-08D2E20B091F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudGeneratorTests", "TagCloudGeneratorTests\TagCloudGeneratorTests.csproj", "{D619ECBB-121A-4B3C-AFB5-279973A1C434}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TagCloudUIClient", "TagCloudUIClient\TagCloudUIClient.csproj", "{43BBA4FF-A397-4EA9-8F42-E6E2BB8F48F1}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -32,6 +43,25 @@ Global {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Debug|Any CPU.Build.0 = Debug|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.ActiveCfg = Release|Any CPU {4B77EC28-5FB5-4095-B3D7-127F5C488D6E}.Release|Any CPU.Build.0 = Release|Any CPU + {C824111A-FF7E-42C9-A4F3-A912E6BD39E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C824111A-FF7E-42C9-A4F3-A912E6BD39E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C824111A-FF7E-42C9-A4F3-A912E6BD39E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C824111A-FF7E-42C9-A4F3-A912E6BD39E9}.Release|Any CPU.Build.0 = Release|Any CPU + {2C8600EB-F62C-42EB-9EAA-08D2E20B091F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C8600EB-F62C-42EB-9EAA-08D2E20B091F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C8600EB-F62C-42EB-9EAA-08D2E20B091F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C8600EB-F62C-42EB-9EAA-08D2E20B091F}.Release|Any CPU.Build.0 = Release|Any CPU + {D619ECBB-121A-4B3C-AFB5-279973A1C434}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D619ECBB-121A-4B3C-AFB5-279973A1C434}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D619ECBB-121A-4B3C-AFB5-279973A1C434}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D619ECBB-121A-4B3C-AFB5-279973A1C434}.Release|Any CPU.Build.0 = Release|Any CPU + {43BBA4FF-A397-4EA9-8F42-E6E2BB8F48F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43BBA4FF-A397-4EA9-8F42-E6E2BB8F48F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43BBA4FF-A397-4EA9-8F42-E6E2BB8F48F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43BBA4FF-A397-4EA9-8F42-E6E2BB8F48F1}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {C33F3A5E-A1ED-4657-9B35-968A4CB23AA1} = {754C1CC8-A8B6-46C6-B35C-8A43B80111A0} From bda341856a066bf43fb10367a1603b6c5427c8ef Mon Sep 17 00:00:00 2001 From: Xineev Date: Mon, 29 Dec 2025 19:47:00 +0300 Subject: [PATCH 02/13] Implemented Result pattern --- TagCloudConsoleClient/ConsoleClient.cs | 29 +++----- .../Core/Interfaces/IFormatReader.cs | 2 +- .../Core/Interfaces/IReaderRepository.cs | 2 +- .../Core/Interfaces/IRenderer.cs | 2 +- .../Core/Interfaces/ITagCloudGenerator.cs | 2 +- .../Infrastructure/Readers/DocxReader.cs | 11 +-- .../Readers/ReaderRepository.cs | 14 ++-- .../Infrastructure/Readers/TxtReader.cs | 12 +--- .../Infrastructure/Renderers/PngRenderer.cs | 47 +++++++++---- TagCloudUIClient/MainForm.Designer.cs | 68 ++++++++----------- 10 files changed, 87 insertions(+), 102 deletions(-) diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index 1aef52eb4..1147a4d2e 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -58,27 +58,16 @@ private void RunWithOptions(Options opts) 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 + _readersRepository.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/IFormatReader.cs b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs index a39bbe17a..0575196d7 100644 --- a/TagCloudGenerator/Core/Interfaces/IFormatReader.cs +++ b/TagCloudGenerator/Core/Interfaces/IFormatReader.cs @@ -5,6 +5,6 @@ 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/IReaderRepository.cs b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs index 9a96b90b7..83f5ba476 100644 --- a/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs +++ b/TagCloudGenerator/Core/Interfaces/IReaderRepository.cs @@ -4,6 +4,6 @@ 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 3c60b7f5f..b4eea0d2e 100644 --- a/TagCloudGenerator/Core/Interfaces/IRenderer.cs +++ b/TagCloudGenerator/Core/Interfaces/IRenderer.cs @@ -6,6 +6,6 @@ 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 23b996aff..429ab2f49 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -6,6 +6,6 @@ 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/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 8126d4c94..8923adcfe 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -11,9 +11,9 @@ 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 @@ -21,12 +21,7 @@ public List TryRead(string filePath) .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 dfc8e973d..4ca96e4c6 100644 --- a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -16,13 +16,17 @@ 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"); + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); - return true; + 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 a14801572..1d5a54613 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 cd33762a2..ad80214af 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -7,24 +7,41 @@ 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); - using var graphics = Graphics.FromImage(bitmap); - ConfigureGraphics(graphics); - graphics.Clear(canvasSettings.BackgroundColor); - 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, canvasSettings, textSettings, brush, pen, stringFormat); - } + if (itemsList.Count == 0) + return Result.Fail("Cloud items are empty"); - return bitmap; + if (canvasSettings.CanvasSize.Width <= 0 || + canvasSettings.CanvasSize.Height <= 0) + return Result.Fail("Invalid canvas size"); + + var cloudBounds = CalculateCloudBounds(itemsList); + 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); + ConfigureGraphics(graphics); + graphics.Clear(canvasSettings.BackgroundColor); + + 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, canvasSettings, textSettings, brush, pen, stringFormat); + return bitmap; + }, "Failed to render tag cloud image"); } private (int offsetX, int offsetY) CalculateOffset( diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs index 488649b77..3cdd20cf6 100644 --- a/TagCloudUIClient/MainForm.Designer.cs +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -67,26 +67,22 @@ 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 + + _readersRepository.TryGetReader(path) + .Then(reader => reader.TryRead(path)) + .Then(words => _normalizer.Normalize(words)) + .OnSuccess(words => { - throw new Exception("no suitable formate readers found"); - } - } - catch (Exception ex) - { - MessageBox.Show( - $"Ошибка чтения: {ex.Message}", - "Формат файла не поддерживается", - MessageBoxButtons.OK, - MessageBoxIcon.Error); - } + 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); @@ -155,29 +151,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); - } + }); } From 0a21c47e6ca2597c25f46e25b285d242839e7ab3 Mon Sep 17 00:00:00 2001 From: Xineev Date: Mon, 29 Dec 2025 19:51:05 +0300 Subject: [PATCH 03/13] Updated tests with new exceptions handling pattern --- .../Core/Services/CloudGenerator.cs | 4 +- TagCloudGeneratorTests/CloudGeneratorTests.cs | 12 ++--- TagCloudGeneratorTests/DocxReaderTests.cs | 13 +++-- .../ReaderRepositoryTests.cs | 14 ++--- TagCloudGeneratorTests/TxtReaderTests.cs | 51 +++++++++++-------- 5 files changed, 52 insertions(+), 42 deletions(-) diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index 34e2f363b..c83405da9 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -33,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/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index e380fab23..402bab9de 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,8 +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 ec47fda1d..0198c87a5 100644 --- a/TagCloudGeneratorTests/DocxReaderTests.cs +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -42,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] @@ -52,7 +53,8 @@ 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] @@ -61,15 +63,16 @@ 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 52d956545..e9c7f6435 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 e9116c8e7..d9d8825f3 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) From 52f7aed8bee26a33f803607df910e12502853f4f Mon Sep 17 00:00:00 2001 From: Xineev Date: Sat, 3 Jan 2026 21:44:05 +0300 Subject: [PATCH 04/13] Implemented Result pattern for more classes and added fixes --- TagCloudConsoleClient/ConsoleClient.cs | 12 ++-- .../Algorithms/BasicTagCloudAlgorithm.cs | 30 +++------ .../Core/Interfaces/IAnalyzer.cs | 4 +- TagCloudGenerator/Core/Interfaces/IClient.cs | 3 +- TagCloudGenerator/Core/Interfaces/IFilter.cs | 7 ++- .../Core/Interfaces/INormalizer.cs | 6 +- TagCloudGenerator/Core/Interfaces/ISorter.cs | 10 +++ .../Core/Interfaces/ISorterer.cs | 8 --- .../Core/Interfaces/ITagCloudAlgorithm.cs | 4 +- .../Core/Interfaces/ITextMeasurer.cs | 3 +- .../Core/Models/WordFrequencyData.cs | 10 +++ .../Core/Models/WordsWithFrequency.cs | 12 ++++ .../Core/Services/CloudGenerator.cs | 50 ++++++++------- TagCloudGenerator/DI/TagCloudModule.cs | 5 +- .../Analyzers/WordsFrequencyAnalyzer.cs | 19 +++--- .../Filters/BoringWordsFilter.cs | 9 ++- .../Measurers/GraphicsTextMeasurer.cs | 12 +++- .../Normalizers/LowerCaseNormalizer.cs | 7 ++- .../Infrastructure/Readers/DocxReader.cs | 16 ++++- .../Readers/ReaderRepository.cs | 4 +- .../Infrastructure/Readers/TxtReader.cs | 4 +- .../Infrastructure/Renderers/PngRenderer.cs | 62 +++++++------------ TagCloudGenerator/Infrastructure/Result.cs | 3 +- .../Sorterers/FrequencyDescendingSorterer.cs | 16 ----- .../Sorters/FrequencyDescendingSorter.cs | 36 +++++++++++ 25 files changed, 204 insertions(+), 148 deletions(-) create mode 100644 TagCloudGenerator/Core/Interfaces/ISorter.cs delete mode 100644 TagCloudGenerator/Core/Interfaces/ISorterer.cs create mode 100644 TagCloudGenerator/Core/Models/WordFrequencyData.cs create mode 100644 TagCloudGenerator/Core/Models/WordsWithFrequency.cs delete mode 100644 TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs create mode 100644 TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index 1147a4d2e..5f0ef8f0b 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -4,8 +4,6 @@ using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; using TagCloudGenerator.Infrastructure; -using TagCloudGenerator.Infrastructure.Readers; -using static System.Runtime.InteropServices.JavaScript.JSType; namespace TagCloudConsoleClient { @@ -50,8 +48,8 @@ private void RunWithOptions(Options opts) .SetFontSizeRange(opts.MinFontSize, opts.MaxFontSize) .SetTextColor(TryParseColor(opts.TextColor)); - string inputFile = opts.InputFile; - string outputFile = opts.OutputFile; + var inputFile = opts.InputFile; + var outputFile = opts.OutputFile; Console.WriteLine("Starting tag cloud generation..."); Console.WriteLine($"Input file: {inputFile}"); @@ -62,7 +60,11 @@ private void RunWithOptions(Options opts) .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")) + .Then(image => Result.OfAction(() => + { + image.Save(outputFile, ImageFormat.Png); + image.Dispose(); + }, "Failed to save output image")) .OnFail(error => { Console.WriteLine("Error during generation:"); diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs index 403a5bf13..1992e3e0e 100644 --- a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -1,5 +1,6 @@ using System.Drawing; using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Algorithms { @@ -7,42 +8,38 @@ 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"); + if (center.X < 0 || center.Y < 0) + center = new Point(0, 0); + this.center = center; } public BasicTagCloudAlgorithm() { - this.center = new Point(0, 0); + center = new Point(0, 0); } - public Rectangle PutNextRectangle(Size rectangleSize) + public Result PutNextRectangle(Size rectangleSize) { - if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) throw new ArgumentException("Rectangle sizes must be positives"); + if (rectangleSize.Width <= 0 || rectangleSize.Height <= 0) + return Result.Fail("Rectangle sizes must be positives"); if (rectangles.Count == 0) { - return PutFirstRectangle(rectangleSize); + return Result.Ok(PutFirstRectangle(rectangleSize)); } - return PlaceNextRectange(rectangleSize); + return Result.Ok(PlaceNextRectange(rectangleSize)); } private Rectangle PlaceNextRectange(Size rectangleSize) @@ -84,13 +81,6 @@ private Rectangle PutFirstRectangle(Size rectangleSize) 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; - } - public ITagCloudAlgorithm Reset() { rectangles.Clear(); diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs index 8c47d59dd..f1b8e1c08 100644 --- a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -1,9 +1,9 @@ -using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface IAnalyzer { - Dictionary Analyze(List words); + Result> Analyze(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/IClient.cs b/TagCloudGenerator/Core/Interfaces/IClient.cs index de0e1098c..20ce48af8 100644 --- a/TagCloudGenerator/Core/Interfaces/IClient.cs +++ b/TagCloudGenerator/Core/Interfaces/IClient.cs @@ -1,4 +1,5 @@ -namespace TagCloudGenerator.Core.Interfaces + +namespace TagCloudGenerator.Core.Interfaces { public interface IClient { diff --git a/TagCloudGenerator/Core/Interfaces/IFilter.cs b/TagCloudGenerator/Core/Interfaces/IFilter.cs index e660e7827..2240dc388 100644 --- a/TagCloudGenerator/Core/Interfaces/IFilter.cs +++ b/TagCloudGenerator/Core/Interfaces/IFilter.cs @@ -1,8 +1,11 @@ - +using TagCloudGenerator.Infrastructure; + namespace TagCloudGenerator.Core.Interfaces { public interface IFilter { - List Filter(List words); + Result> Filter(List words); + + bool ShouldInclude(string word); } } diff --git a/TagCloudGenerator/Core/Interfaces/INormalizer.cs b/TagCloudGenerator/Core/Interfaces/INormalizer.cs index daaaf97bc..289a41e30 100644 --- a/TagCloudGenerator/Core/Interfaces/INormalizer.cs +++ b/TagCloudGenerator/Core/Interfaces/INormalizer.cs @@ -1,7 +1,9 @@ -namespace TagCloudGenerator.Core.Interfaces +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces { public interface INormalizer { - public List Normalize(List words); + public Result> Normalize(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/ISorter.cs b/TagCloudGenerator/Core/Interfaces/ISorter.cs new file mode 100644 index 000000000..ee7f15352 --- /dev/null +++ b/TagCloudGenerator/Core/Interfaces/ISorter.cs @@ -0,0 +1,10 @@ +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; + +namespace TagCloudGenerator.Core.Interfaces +{ + public interface ISorter + { + public Result Sort(Dictionary wordsWithFreqs); + } +} diff --git a/TagCloudGenerator/Core/Interfaces/ISorterer.cs b/TagCloudGenerator/Core/Interfaces/ISorterer.cs deleted file mode 100644 index b504e89d6..000000000 --- a/TagCloudGenerator/Core/Interfaces/ISorterer.cs +++ /dev/null @@ -1,8 +0,0 @@ - -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 1331e9b86..a896c0fe7 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudAlgorithm.cs @@ -1,11 +1,11 @@ using System.Drawing; -using TagCloudGenerator.Algorithms; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudAlgorithm { - Rectangle PutNextRectangle(Size rectangleSize); + Result PutNextRectangle(Size rectangleSize); public ITagCloudAlgorithm Reset(); } diff --git a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs index 0f1f73be9..23435663c 100644 --- a/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs +++ b/TagCloudGenerator/Core/Interfaces/ITextMeasurer.cs @@ -1,9 +1,10 @@ using System.Drawing; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface ITextMeasurer { - Size Measure(string word, float fontSize, string fontFamily); + Result Measure(string word, float fontSize, string fontFamily); } } diff --git a/TagCloudGenerator/Core/Models/WordFrequencyData.cs b/TagCloudGenerator/Core/Models/WordFrequencyData.cs new file mode 100644 index 000000000..30a2896ae --- /dev/null +++ b/TagCloudGenerator/Core/Models/WordFrequencyData.cs @@ -0,0 +1,10 @@ + +namespace TagCloudGenerator.Core.Models +{ + public class WordFrequencyData + { + public string Word { get; set; } + public int Frequency { get; set; } + public WordFrequencyData() { } + } +} diff --git a/TagCloudGenerator/Core/Models/WordsWithFrequency.cs b/TagCloudGenerator/Core/Models/WordsWithFrequency.cs new file mode 100644 index 000000000..5c607a88c --- /dev/null +++ b/TagCloudGenerator/Core/Models/WordsWithFrequency.cs @@ -0,0 +1,12 @@ + +namespace TagCloudGenerator.Core.Models +{ + public class WordsWithFrequency + { + public List WordsWithFreq { get; set; } + public int MaxFreq { get; set; } + public int MinFreq { get; set; } + + public WordsWithFrequency() { } + } +} diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index c83405da9..ea7ebfdaf 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -1,9 +1,7 @@ using System.Drawing; -using TagCloudGenerator.Algorithms; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; using TagCloudGenerator.Infrastructure; -using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudGenerator.Core.Services { @@ -14,57 +12,61 @@ public class CloudGenerator : ITagCloudGenerator private readonly IRenderer _renderer; private readonly IFontSizeCalculator _fontSizeCalculator; private readonly ITextMeasurer _textMeasurer; - private readonly ISorterer _sorterer; - private readonly Point _center; + private readonly ISorter _sorter; public CloudGenerator(ITagCloudAlgorithm algorithm, IAnalyzer analyzer, IRenderer renderer, IFontSizeCalculator fontSizeCalculator, ITextMeasurer textMeasurer, - ISorterer sorterer) + ISorter sorterer) { - this._algorithm = algorithm; + _algorithm = algorithm; _analyzer = analyzer; _renderer = renderer; _fontSizeCalculator = fontSizeCalculator; _textMeasurer = textMeasurer; - _sorterer = sorterer; - _center = new Point(0, 0); + _sorter = sorterer; } public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) { - words = ApplyFilters(words, filters); - if (words.Count == 0) return Result.Fail("Found no words to render after filtering"); - - var wordsWithFreq = _sorterer.Sort(_analyzer.Analyze(words)); - - var initializedItems = InitializeCloudItems(wordsWithFreq, textSettings).ToList(); - _algorithm.Reset(); + + var filteredWords = ApplyFilters(words, filters); + + var wordsWithFreq = _analyzer.Analyze(filteredWords); + var sortedWords = _sorter.Sort(wordsWithFreq.Value); + var initializedItems = InitializeCloudItems(sortedWords.Value, textSettings).ToList(); return _renderer.Render(initializedItems, canvasSettings, textSettings); } private List ApplyFilters(List words, IEnumerable filters) { - var filteredWords = words; - foreach (var filter in filters) + var filteredWords = new List(); + + foreach (var word in words) { - filteredWords = filter.Filter(filteredWords); + foreach (var filter in filters) + { + if (filter.ShouldInclude(word) == false) + break; + } + filteredWords.Add(word); } + return filteredWords; } - private IEnumerable InitializeCloudItems(IEnumerable<(string Word, int Frequency)> items, TextSettings settings) + private IEnumerable InitializeCloudItems(WordsWithFrequency items, TextSettings settings) { var itemsList = new List(); - var minFrequency = items.Min(i => i.Frequency); - var maxFrequency = items.Max(i => i.Frequency); + var minFrequency = items.MinFreq; + var maxFrequency = items.MaxFreq; - foreach (var item in items) + foreach (var item in items.WordsWithFreq) { var fontSize = _fontSizeCalculator.Calculate( item.Frequency, @@ -78,12 +80,12 @@ private IEnumerable InitializeCloudItems(IEnumerable<(string Word, in fontSize, settings.FontFamily); - var itemRectangle = _algorithm.PutNextRectangle(textSize); + var itemRectangle = _algorithm.PutNextRectangle(textSize.Value); itemsList.Add( new CloudItem( word: item.Word, - rectangle: itemRectangle, + rectangle: itemRectangle.Value, fontSize: fontSize, textColor: settings.TextColor, fontFamily: settings.FontFamily, diff --git a/TagCloudGenerator/DI/TagCloudModule.cs b/TagCloudGenerator/DI/TagCloudModule.cs index 3df3fe708..5c36a9a22 100644 --- a/TagCloudGenerator/DI/TagCloudModule.cs +++ b/TagCloudGenerator/DI/TagCloudModule.cs @@ -4,7 +4,6 @@ using TagCloudGenerator.Core.Services; using TagCloudGenerator.Infrastructure.Analyzers; using TagCloudGenerator.Infrastructure.Calculators; -using TagCloudGenerator.Infrastructure.Filters; using TagCloudGenerator.Infrastructure.Measurers; using TagCloudGenerator.Infrastructure.Normalizers; using TagCloudGenerator.Infrastructure.Readers; @@ -13,7 +12,7 @@ namespace TagCloudGenerator.DI { - public class TagCloudModule : Autofac.Module + public class TagCloudModule : Module { protected override void Load(ContainerBuilder builder) { @@ -24,7 +23,7 @@ protected override void Load(ContainerBuilder builder) .As() .SingleInstance(); - builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index f63083826..9b320e7ab 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -1,26 +1,27 @@ -using System.Drawing; -using TagCloudGenerator.Core.Interfaces; -using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Core.Interfaces; namespace TagCloudGenerator.Infrastructure.Analyzers { public class WordsFrequencyAnalyzer : IAnalyzer { - public Dictionary Analyze(List words) + public Result> Analyze(List words) { var wordFreqDictionary = new Dictionary(); - if (words == null || words.Count == 0) - return new Dictionary(); + if (words == null) + return Result.Fail>("Missing words list to analyze"); foreach (string word in words) { - if(wordFreqDictionary.TryAdd(word, 1)) + if(word == null) + return Result.Fail>("Word in a list is null"); + + if (wordFreqDictionary.TryAdd(word, 1)) continue; - else wordFreqDictionary[word]++; + wordFreqDictionary[word]++; } - return wordFreqDictionary; + return Result.Ok(wordFreqDictionary); } } } diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs index 4cfdbd033..1ab530c17 100644 --- a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -13,15 +13,20 @@ public BoringWordsFilter(IEnumerable words) boringWords = words.ToArray(); } - public List Filter(List words) + public Result> Filter(List words) { - if (words == null || words.Count == 0) return new List(); + if (words == null) + return Result.Fail>("Filter missing list of words"); return words.Where(w => !boringWords.Contains(w)).ToList(); } + //А надо ли тут оборачивать в Result? + //С одной стороны оставляя bool мы маскируем исключение, с другой Result исполняет аналогичную возвращаемому значению задачу public bool ShouldInclude(string word) { + if(word == null) return false; + return !boringWords.Contains(word); } } diff --git a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs index 5ecdd40c0..5c575781f 100644 --- a/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs +++ b/TagCloudGenerator/Infrastructure/Measurers/GraphicsTextMeasurer.cs @@ -5,14 +5,22 @@ namespace TagCloudGenerator.Infrastructure.Measurers { public class GraphicsTextMeasurer : ITextMeasurer { - public Size Measure(string word, float fontSize, string fontFamily) + public Result Measure(string word, float fontSize, string fontFamily) { + if (fontSize < 0) + return Result.Fail("Font size can't be negative number"); + + if(word == null) + return Result.Fail("Can't measure word which is null"); + 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)); + return Result.Ok( + new Size((int)Math.Ceiling(size.Width), + (int)Math.Ceiling(size.Height))); } } } diff --git a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs index 83ef709bb..b6745ce03 100644 --- a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs +++ b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs @@ -4,11 +4,12 @@ namespace TagCloudGenerator.Infrastructure.Normalizers { public class LowerCaseNormalizer : INormalizer { - public List Normalize(List words) + public Result> Normalize(List words) { - if (words == null || words.Count == 0) return new List(); + if (words == null) + return Result.Fail>("Can't normalize if list is null"); - return words.Select(w => w.ToLower()).ToList(); + return Result.Of(() => words.Select(w => w.ToLower()).ToList()); } } } diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 8923adcfe..1067b093c 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -13,15 +13,25 @@ public bool CanRead(string filePath) public Result> TryRead(string filePath) { - return Result.Of(() => + using var doc = WordprocessingDocument.Open(filePath, false); + + if (doc.MainDocumentPart == null) + return Result.Fail>("Missing MainDocumentPart"); + + if (doc.MainDocumentPart.Document.Body == null) + return Result.Fail>("Missing document body"); + + var result = 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(); - }, "Failed to read DOCX file"); + }, + $"Failed to load .docx document: '{filePath}'"); + + return result; } } } diff --git a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs index 4ca96e4c6..0e5936859 100644 --- a/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs +++ b/TagCloudGenerator/Infrastructure/Readers/ReaderRepository.cs @@ -20,11 +20,11 @@ public Result TryGetReader(string filePath) { if (string.IsNullOrWhiteSpace(filePath)) return Result.Fail("File path is empty"); + var reader = _readers.FirstOrDefault(r => r.CanRead(filePath)); if (reader == null) - return Result.Fail( - $"No reader found for file '{filePath}'"); + return Result.Fail($"No reader found for file '{filePath}'"); return Result.Ok(reader); } diff --git a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs index 1d5a54613..4f7ba1cbc 100644 --- a/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/TxtReader.cs @@ -11,7 +11,9 @@ public bool CanRead(string filePath) public Result> TryRead(string filePath) { - return Result.Of(() => { return File.ReadAllLines(filePath).ToList(); }, "Faield to read .txt file"); + return Result.Of(() => + File.ReadAllLines(filePath).ToList(), + $"Failed to read .txt file: '{filePath}'"); } } } diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index ad80214af..1558b0141 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -1,5 +1,4 @@ using System.Drawing; -using System.Drawing.Imaging; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; @@ -13,35 +12,38 @@ public Result Render(IEnumerable items, CanvasSettings canvas return Result.Fail("Cloud items are null"); var itemsList = items.ToList(); - if (itemsList.Count == 0) - return Result.Fail("Cloud items are empty"); if (canvasSettings.CanvasSize.Width <= 0 || canvasSettings.CanvasSize.Height <= 0) return Result.Fail("Invalid canvas size"); - var cloudBounds = CalculateCloudBounds(itemsList); - 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 (Graphics graphics = Graphics.FromImage(bitmap)) { - var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); - using var graphics = Graphics.FromImage(bitmap); ConfigureGraphics(graphics); graphics.Clear(canvasSettings.BackgroundColor); - 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, canvasSettings, textSettings, brush, pen, stringFormat); - return bitmap; - }, "Failed to render tag cloud image"); + using (SolidBrush brush = new SolidBrush(textSettings.TextColor)) + { + using (StringFormat stringFormat = new StringFormat()) + { + stringFormat.Alignment = StringAlignment.Center; + stringFormat.LineAlignment = StringAlignment.Center; + + using (Pen pen = new Pen(textSettings.TextColor, 1)) + { + pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; + + return Result.Of(() => + { + foreach (var item in itemsList) + DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); + return bitmap; + }); + } + } + } + } } private (int offsetX, int offsetY) CalculateOffset( @@ -79,8 +81,6 @@ private void DrawCloudItem( item.Rectangle.Width, item.Rectangle.Height); - var color = item.TextColor ?? textSettings.TextColor; - using var font = new Font( item.FontFamily, item.FontSize, @@ -102,21 +102,5 @@ 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 index 39ca803c5..5106c011c 100644 --- a/TagCloudGenerator/Infrastructure/Result.cs +++ b/TagCloudGenerator/Infrastructure/Result.cs @@ -1,4 +1,5 @@ -namespace TagCloudGenerator.Infrastructure + +namespace TagCloudGenerator.Infrastructure { public class None { diff --git a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs b/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs deleted file mode 100644 index 0bdc20c3e..000000000 --- a/TagCloudGenerator/Infrastructure/Sorterers/FrequencyDescendingSorterer.cs +++ /dev/null @@ -1,16 +0,0 @@ -using TagCloudGenerator.Core.Interfaces; - -namespace TagCloudGenerator.Infrastructure.Sorterers -{ - 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/Infrastructure/Sorters/FrequencyDescendingSorter.cs b/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs new file mode 100644 index 000000000..6911fa252 --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs @@ -0,0 +1,36 @@ +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; + +namespace TagCloudGenerator.Infrastructure.Sorterers +{ + public class FrequencyDescendingSorter : ISorter + { + public Result Sort(Dictionary wordsWithFreqs) + { + if (wordsWithFreqs == null) + return Result.Fail("Found no words to sort"); + + var minFreq = int.MaxValue; + var maxFreq = int.MinValue; + + var words = new List(); + + foreach (var word in wordsWithFreqs) + { + if(word.Key == null) + return Result.Fail("Sorter found null key in dictionary"); + + var wordWithFreq = new WordFrequencyData { Word = word.Key, Frequency = word.Value }; + minFreq = Math.Min(minFreq, word.Value); + maxFreq = Math.Max(maxFreq, word.Value); + + words.Add(wordWithFreq); + } + + var sorted = words.OrderByDescending(w => w.Frequency).ThenBy(w => w.Word).ToList(); + + var result = Result.Ok(new WordsWithFrequency { WordsWithFreq = sorted, MaxFreq = maxFreq, MinFreq = minFreq }); + return result; + } + } +} From 57591cac7eb004f0b512e193e7474c1e324cd4c8 Mon Sep 17 00:00:00 2001 From: Xineev Date: Sat, 3 Jan 2026 23:30:19 +0300 Subject: [PATCH 05/13] Made critical adjustments for classes --- TagCloudGenerator/Core/Services/CloudGenerator.cs | 6 ++++++ .../Infrastructure/Readers/DocxReader.cs | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index ea7ebfdaf..e78cc686e 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -36,7 +36,13 @@ public Result Generate(List words, CanvasSettings canvasSettings var filteredWords = ApplyFilters(words, filters); var wordsWithFreq = _analyzer.Analyze(filteredWords); + if (!wordsWithFreq.IsSuccess || wordsWithFreq.Value == null) + return Result.Fail("Analyzed words collection is null"); + var sortedWords = _sorter.Sort(wordsWithFreq.Value); + if (!sortedWords.IsSuccess || sortedWords.Value == null) + return Result.Fail("Sorted words collection is null"); + var initializedItems = InitializeCloudItems(sortedWords.Value, textSettings).ToList(); return _renderer.Render(initializedItems, canvasSettings, textSettings); diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 1067b093c..0d19ed2c9 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -13,7 +13,14 @@ public bool CanRead(string filePath) public Result> TryRead(string filePath) { - using var doc = WordprocessingDocument.Open(filePath, false); + var docResult = Result.Of(() => + WordprocessingDocument.Open(filePath, false), + $"Failed to get document: '{filePath}'"); + + if (!docResult.IsSuccess) + return Result.Fail>(docResult.Error); + + var doc = docResult.Value; if (doc.MainDocumentPart == null) return Result.Fail>("Missing MainDocumentPart"); @@ -31,6 +38,8 @@ public Result> TryRead(string filePath) }, $"Failed to load .docx document: '{filePath}'"); + doc.Dispose(); + return result; } } From a1fdfe0ff8fe08bb2865a85572a5c18313266de5 Mon Sep 17 00:00:00 2001 From: Xineev Date: Sat, 3 Jan 2026 23:30:57 +0300 Subject: [PATCH 06/13] Updated tag cloud tests --- .../BoringWordsFilterTests.cs | 44 ++++++++++++------- TagCloudGeneratorTests/CloudGeneratorTests.cs | 28 ++++++++---- TagCloudGeneratorTests/DocxReaderTests.cs | 1 - .../GraphicsTextMeasurerTests.cs | 30 ++++++------- .../WordsFrequencyAnalyzerTests.cs | 8 ++-- 5 files changed, 66 insertions(+), 45 deletions(-) diff --git a/TagCloudGeneratorTests/BoringWordsFilterTests.cs b/TagCloudGeneratorTests/BoringWordsFilterTests.cs index 52232f6ab..861ed6f03 100644 --- a/TagCloudGeneratorTests/BoringWordsFilterTests.cs +++ b/TagCloudGeneratorTests/BoringWordsFilterTests.cs @@ -11,7 +11,7 @@ public class BoringWordsFilterTests public void Filter_EmptyInput_ReturnsEmpty() { var words = new List(); - var result = filter.Filter(words); + var result = filter.Filter(words).GetValueOrThrow(); Assert.That(result, Is.Empty); } @@ -19,21 +19,25 @@ public void Filter_EmptyInput_ReturnsEmpty() public void Filter_RemovesBoringWords_Test() { var words = new List { "hello", "in", "world", "a", "test" }; - var result = filter.Filter(words).ToList(); + var result = filter.Filter(words); + var actual = new List(); + if (result.IsSuccess) + actual = result.GetValueOrThrow(); + - 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")); + Assert.That(actual.Count, Is.EqualTo(3)); + Assert.That(actual, Contains.Item("hello")); + Assert.That(actual, Contains.Item("world")); + Assert.That(actual, Contains.Item("test")); + Assert.That(actual, Does.Not.Contain("in")); + Assert.That(actual, Does.Not.Contain("a")); } [Test] public void Filter_AllBoringWords_ReturnsEmpty_Test() { var words = new List { "in", "a", "for", "on" }; - var result = filter.Filter(words); + var result = filter.Filter(words).GetValueOrThrow(); Assert.That(result, Is.Empty); } @@ -41,10 +45,13 @@ public void Filter_AllBoringWords_ReturnsEmpty_Test() public void Filter_NoBoringWords_ReturnsAllWords_Test() { var words = new List { "hello", "world", "test" }; - var result = filter.Filter(words).ToList(); + var result = filter.Filter(words); + var actual = new List(); + if (result.IsSuccess) + actual = result.GetValueOrThrow(); - Assert.That(result.Count, Is.EqualTo(3)); - Assert.That(result, Is.EquivalentTo(words)); + Assert.That(actual.Count, Is.EqualTo(3)); + Assert.That(actual, Is.EquivalentTo(words)); } [Test] @@ -71,12 +78,15 @@ public void ShouldInclude_ReturnsTrueForNormalWords_Test() public void Filter_PreservesOrder_Test() { var words = new List { "hello", "in", "world", "a", "test", "for" }; - var result = filter.Filter(words).ToList(); + var result = filter.Filter(words); + var actual = new List(); + if (result.IsSuccess) + actual = result.GetValueOrThrow(); - 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")); + Assert.That(actual.Count, Is.EqualTo(3)); + Assert.That(actual[0], Is.EqualTo("hello")); + Assert.That(actual[1], Is.EqualTo("world")); + Assert.That(actual[2], Is.EqualTo("test")); } } } diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index 402bab9de..d39c7cd5f 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -4,6 +4,7 @@ using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; using TagCloudGenerator.Core.Services; +using TagCloudGenerator.Infrastructure; namespace TagCloudGeneratorTests { @@ -15,7 +16,7 @@ public class CloudGeneratorTests private Mock rendererMock; private Mock fontSizeCalculatorMock; private Mock textMeasurerMock; - private Mock sortererMock; + private Mock sortererMock; private CloudGenerator cloudGenerator; [SetUp] @@ -27,7 +28,7 @@ public void Setup() rendererMock = new Mock(); fontSizeCalculatorMock = new Mock(); textMeasurerMock = new Mock(); - sortererMock = new Mock(); + sortererMock = new Mock(); cloudGenerator = new CloudGenerator( algorithmMock.Object, @@ -64,10 +65,12 @@ public void Generate_EmptyWords_ReturnsFailResult_AndDoesNotRender_Test() public void Generate_AllWordsFilteredOut_ReturnsFailResult_Test() { var words = new List { "in", "a", "for" }; + + var filtered = new List(); filterMock .Setup(f => f.Filter(words)) - .Returns(new List()); + .Returns(filtered); var result = cloudGenerator.Generate( words, @@ -92,11 +95,20 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() var analyzed = new Dictionary { { "hello", 2 }, { "world", 1 } }; - var sorted = new List<(string Word, int Frequency)> { ("hello", 2), ("world", 1) }; + var sorted = new WordsWithFrequency + { + WordsWithFreq = new List + { + new WordFrequencyData { Word = "hello", Frequency = 2 }, + new WordFrequencyData { Word = "world", Frequency = 1 } + }, + MaxFreq = 2, + MinFreq = 1 + }; filterMock - .Setup(f => f.Filter(words)) - .Returns(filteredWords); + .Setup(f => f.ShouldInclude(It.IsAny())) + .Returns(true); analyzerMock .Setup(a => a.Analyze(filteredWords)) @@ -104,7 +116,7 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() sortererMock .Setup(s => s.Sort(analyzed)) - .Returns(sorted); + .Returns(Result.Ok(sorted)); var textSettings = new TextSettings() .SetFontFamily("Arial") @@ -146,7 +158,7 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() Assert.That(result.IsSuccess, Is.True); result.GetValueOrThrow().Dispose(); - filterMock.Verify(f => f.Filter(words), Times.Once); + filterMock.Verify(f => f.ShouldInclude(It.IsAny()), Times.Exactly(3)); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); algorithmMock.Verify(a => a.Reset(), Times.Once); algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); diff --git a/TagCloudGeneratorTests/DocxReaderTests.cs b/TagCloudGeneratorTests/DocxReaderTests.cs index 0198c87a5..f1d5fb806 100644 --- a/TagCloudGeneratorTests/DocxReaderTests.cs +++ b/TagCloudGeneratorTests/DocxReaderTests.cs @@ -1,7 +1,6 @@ using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Wordprocessing; using NUnit.Framework; -using TagCloudGenerator.Infrastructure; using TagCloudGenerator.Infrastructure.Readers; namespace TagCloudGeneratorTests diff --git a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs index ecfa6b634..49125e1a4 100644 --- a/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs +++ b/TagCloudGeneratorTests/GraphicsTextMeasurerTests.cs @@ -10,7 +10,7 @@ public class GraphicsTextMeasurerTests [Test] public void Measure_EmptyString_ReturnsValidSize_Test() { - var result = measurer.Measure("", 12f, "Arial"); + var result = measurer.Measure("", 12f, "Arial").GetValueOrThrow(); Assert.That(result.Width, Is.GreaterThanOrEqualTo(0)); Assert.That(result.Height, Is.GreaterThanOrEqualTo(0)); } @@ -18,7 +18,7 @@ public void Measure_EmptyString_ReturnsValidSize_Test() [Test] public void Measure_SingleCharacter_ReturnsNonZeroSize_Test() { - var result = measurer.Measure("A", 12f, "Arial"); + var result = measurer.Measure("A", 12f, "Arial").GetValueOrThrow(); Assert.That(result.Width, Is.GreaterThan(0)); Assert.That(result.Height, Is.GreaterThan(0)); } @@ -26,16 +26,16 @@ public void Measure_SingleCharacter_ReturnsNonZeroSize_Test() [Test] public void Measure_LongerWord_ReturnsWiderSize_Test() { - var shortSize = measurer.Measure("A", 12f, "Arial"); - var longSize = measurer.Measure("ABCDEFGHIJ", 12f, "Arial"); + var shortSize = measurer.Measure("A", 12f, "Arial").GetValueOrThrow(); + var longSize = measurer.Measure("ABCDEFGHIJ", 12f, "Arial").GetValueOrThrow(); Assert.That(longSize.Width, Is.GreaterThan(shortSize.Width)); } [Test] public void Measure_LargerFont_ReturnsLargerSize_Test() { - var smallSize = measurer.Measure("Test", 12f, "Arial"); - var largeSize = measurer.Measure("Test", 24f, "Arial"); + var smallSize = measurer.Measure("Test", 12f, "Arial").GetValueOrThrow(); + var largeSize = measurer.Measure("Test", 24f, "Arial").GetValueOrThrow(); Assert.That(largeSize.Width, Is.GreaterThan(smallSize.Width)); Assert.That(largeSize.Height, Is.GreaterThan(smallSize.Height)); } @@ -43,8 +43,8 @@ public void Measure_LargerFont_ReturnsLargerSize_Test() [Test] public void Measure_DifferentFontFamily_ReturnsDifferentSize_Test() { - var arialSize = measurer.Measure("Test", 12f, "Arial"); - var timesSize = measurer.Measure("Test", 12f, "Times New Roman"); + var arialSize = measurer.Measure("Test", 12f, "Arial").GetValueOrThrow(); + var timesSize = measurer.Measure("Test", 12f, "Times New Roman").GetValueOrThrow(); Assert.That(timesSize.Width, Is.GreaterThan(0)); Assert.That(timesSize.Height, Is.GreaterThan(0)); } @@ -52,24 +52,24 @@ public void Measure_DifferentFontFamily_ReturnsDifferentSize_Test() [Test] public void Measure_SameParametersTwice_ReturnsSameSize_Test() { - 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)); + var firstSize = measurer.Measure("Consistent", 16f, "Arial").GetValueOrThrow(); + var secondSize = measurer.Measure("Consistent", 16f, "Arial").GetValueOrThrow(); + Assert.That(firstSize.Width, Is.EqualTo(secondSize.Width)); + Assert.That(firstSize.Height, Is.EqualTo(secondSize.Height)); } [Test] public void Measure_WithSpaces_ReturnsCorrectSize_Test() { - var sizeWithoutSpace = measurer.Measure("HelloWorld", 12f, "Arial"); - var sizeWithSpace = measurer.Measure("Hello World", 12f, "Arial"); + var sizeWithoutSpace = measurer.Measure("HelloWorld", 12f, "Arial").GetValueOrThrow(); + var sizeWithSpace = measurer.Measure("Hello World", 12f, "Arial").GetValueOrThrow(); Assert.That(sizeWithSpace.Width, Is.GreaterThan(sizeWithoutSpace.Width)); } [Test] public void Measure_VeryLargeFont_ReturnsProportionalSize_Test() { - var result = measurer.Measure("Test", 100f, "Arial"); + var result = measurer.Measure("Test", 100f, "Arial").GetValueOrThrow(); Assert.That(result.Width, Is.GreaterThan(50)); Assert.That(result.Height, Is.GreaterThan(50)); } diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index f5eb2c556..8fdc1cd9d 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -11,7 +11,7 @@ public class WordsFrequencyAnalyzerTests public void Analyze_EmptyInput_ReturnsEmpty_Test() { var words = new List(); - var result = frequencyAnalyzer.Analyze(words); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); Assert.That(result, Is.Empty); } @@ -19,7 +19,7 @@ public void Analyze_EmptyInput_ReturnsEmpty_Test() public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() { var words = new List { "hello" }; - var result = frequencyAnalyzer.Analyze(words).ToList(); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); Assert.That(result.Count, Is.EqualTo(1)); Assert.That(result[0].Key, Is.EqualTo("hello")); @@ -30,7 +30,7 @@ public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() { var words = new List { "hello", "world", "hello", "test", "world", "hello" }; - var result = frequencyAnalyzer.Analyze(words).ToList(); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); Assert.That(result.Count, Is.EqualTo(3)); Assert.That(result[0].Key, Is.EqualTo("hello")); @@ -45,7 +45,7 @@ public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() { var words = new List { "Hello", "hello", "HELLO" }; - var result = frequencyAnalyzer.Analyze(words).ToList(); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); Assert.That(result.Count, Is.EqualTo(3)); } } From 0ba2794d11eadc79e56f66e55db053cb5959dbaf Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 10 Jan 2026 23:16:20 +0500 Subject: [PATCH 07/13] Updated CloudGenerator and Infrastructure --- .../Algorithms/BasicTagCloudAlgorithm.cs | 4 +- .../Core/Interfaces/IAnalyzer.cs | 5 +- TagCloudGenerator/Core/Interfaces/ISorter.cs | 2 +- .../Core/Interfaces/ITagCloudGenerator.cs | 2 +- .../Core/Services/CloudGenerator.cs | 22 ++++---- .../Analyzers/WordsFrequencyAnalyzer.cs | 51 +++++++++++++++---- .../Filters/BoringWordsFilter.cs | 10 ++-- .../Infrastructure/Filters/NullWordsFilter.cs | 25 +++++++++ .../Normalizers/LowerCaseNormalizer.cs | 4 +- .../Infrastructure/Readers/DocxReader.cs | 12 ++--- .../Infrastructure/Renderers/PngRenderer.cs | 8 +-- .../Sorters/FrequencyDescendingSorter.cs | 27 ++-------- 12 files changed, 105 insertions(+), 67 deletions(-) create mode 100644 TagCloudGenerator/Infrastructure/Filters/NullWordsFilter.cs diff --git a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs index 1992e3e0e..c2155d259 100644 --- a/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs +++ b/TagCloudGenerator/Algorithms/BasicTagCloudAlgorithm.cs @@ -39,10 +39,10 @@ public Result PutNextRectangle(Size rectangleSize) return Result.Ok(PutFirstRectangle(rectangleSize)); } - return Result.Ok(PlaceNextRectange(rectangleSize)); + return Result.Ok(PlaceNextRectangle(rectangleSize)); } - private Rectangle PlaceNextRectange(Size rectangleSize) + private Rectangle PlaceNextRectangle(Size rectangleSize) { while (true) { diff --git a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs index f1b8e1c08..4754975c2 100644 --- a/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs +++ b/TagCloudGenerator/Core/Interfaces/IAnalyzer.cs @@ -1,9 +1,10 @@ -using TagCloudGenerator.Infrastructure; +using TagCloudGenerator.Core.Models; +using TagCloudGenerator.Infrastructure; namespace TagCloudGenerator.Core.Interfaces { public interface IAnalyzer { - Result> Analyze(List words); + Result Analyze(List words); } } diff --git a/TagCloudGenerator/Core/Interfaces/ISorter.cs b/TagCloudGenerator/Core/Interfaces/ISorter.cs index ee7f15352..f16650bc6 100644 --- a/TagCloudGenerator/Core/Interfaces/ISorter.cs +++ b/TagCloudGenerator/Core/Interfaces/ISorter.cs @@ -5,6 +5,6 @@ namespace TagCloudGenerator.Core.Interfaces { public interface ISorter { - public Result Sort(Dictionary wordsWithFreqs); + public Result Sort(WordsWithFrequency wordsWithFreqs); } } diff --git a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs index 429ab2f49..8458af373 100644 --- a/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs +++ b/TagCloudGenerator/Core/Interfaces/ITagCloudGenerator.cs @@ -6,6 +6,6 @@ namespace TagCloudGenerator.Core.Interfaces { public interface ITagCloudGenerator { - public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters); + public Result> Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, ICollection filters); } } diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index e78cc686e..f8821e286 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -29,7 +29,7 @@ public CloudGenerator(ITagCloudAlgorithm algorithm, _sorter = sorterer; } - public Result Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, IEnumerable filters) + public Result> Generate(List words, CanvasSettings canvasSettings, TextSettings textSettings, ICollection filters) { _algorithm.Reset(); @@ -37,29 +37,33 @@ public Result Generate(List words, CanvasSettings canvasSettings var wordsWithFreq = _analyzer.Analyze(filteredWords); if (!wordsWithFreq.IsSuccess || wordsWithFreq.Value == null) - return Result.Fail("Analyzed words collection is null"); + return Result.Fail>("Analyzed words collection is null"); var sortedWords = _sorter.Sort(wordsWithFreq.Value); if (!sortedWords.IsSuccess || sortedWords.Value == null) - return Result.Fail("Sorted words collection is null"); + return Result.Fail>("Sorted words collection is null"); - var initializedItems = InitializeCloudItems(sortedWords.Value, textSettings).ToList(); - - return _renderer.Render(initializedItems, canvasSettings, textSettings); + return InitializeCloudItems(sortedWords.Value, textSettings).ToList(); } - private List ApplyFilters(List words, IEnumerable filters) + private List ApplyFilters(List words, ICollection filters) { var filteredWords = new List(); foreach (var word in words) { + bool shouldInclude = true; foreach (var filter in filters) { - if (filter.ShouldInclude(word) == false) + if (!filter.ShouldInclude(word)) + { + shouldInclude = false; break; + } } - filteredWords.Add(word); + + if (shouldInclude) + filteredWords.Add(word); } return filteredWords; diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index 9b320e7ab..9912fbda3 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -1,27 +1,56 @@ -using TagCloudGenerator.Core.Interfaces; +using DocumentFormat.OpenXml.Office2010.PowerPoint; +using TagCloudGenerator.Core.Interfaces; +using TagCloudGenerator.Core.Models; namespace TagCloudGenerator.Infrastructure.Analyzers { public class WordsFrequencyAnalyzer : IAnalyzer { - public Result> Analyze(List words) + public Result Analyze(List words) { + var wordFreqList = new List(); var wordFreqDictionary = new Dictionary(); + var minFrequency = int.MaxValue; + var maxFrequency = int.MinValue; - if (words == null) - return Result.Fail>("Missing words list to analyze"); + if (words == null || words.Count == 0) + return Result.Ok(new WordsWithFrequency + { + WordsWithFreq = wordFreqList, + MaxFreq = maxFrequency, + MinFreq = minFrequency + }); foreach (string word in words) { - if(word == null) - return Result.Fail>("Word in a list is null"); - - if (wordFreqDictionary.TryAdd(word, 1)) - continue; - wordFreqDictionary[word]++; + if (wordFreqDictionary.TryGetValue(word, out int currentCount)) + { + wordFreqDictionary[word] = currentCount + 1; + int newCount = currentCount + 1; + + minFrequency = Math.Min(newCount, minFrequency); + maxFrequency = Math.Max(newCount, maxFrequency); + } + else + { + wordFreqDictionary[word] = 1; + + minFrequency = Math.Min(1, minFrequency); + maxFrequency = Math.Max(1, maxFrequency); + } } - return Result.Ok(wordFreqDictionary); + foreach (var pair in wordFreqDictionary) + wordFreqList.Add(new WordFrequencyData { + Word = pair.Key, + Frequency = pair.Value + }); + + return Result.Ok(new WordsWithFrequency { + WordsWithFreq = wordFreqList, + MaxFreq = maxFrequency, + MinFreq = minFrequency + }); } } } diff --git a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs index 1ab530c17..157b41f47 100644 --- a/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs +++ b/TagCloudGenerator/Infrastructure/Filters/BoringWordsFilter.cs @@ -15,18 +15,14 @@ public BoringWordsFilter(IEnumerable words) public Result> Filter(List words) { - if (words == null) - return Result.Fail>("Filter missing list of words"); + if (words == null || words.Count == 0) + return Result.Ok(words); - return words.Where(w => !boringWords.Contains(w)).ToList(); + return Result.Ok(words.Where(w => !boringWords.Contains(w)).ToList()); } - //А надо ли тут оборачивать в Result? - //С одной стороны оставляя bool мы маскируем исключение, с другой Result исполняет аналогичную возвращаемому значению задачу public bool ShouldInclude(string word) { - if(word == null) return false; - return !boringWords.Contains(word); } } diff --git a/TagCloudGenerator/Infrastructure/Filters/NullWordsFilter.cs b/TagCloudGenerator/Infrastructure/Filters/NullWordsFilter.cs new file mode 100644 index 000000000..3188b597d --- /dev/null +++ b/TagCloudGenerator/Infrastructure/Filters/NullWordsFilter.cs @@ -0,0 +1,25 @@ +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 NullWordsFilter : IFilter + { + public Result> Filter(List words) + { + if (words == null || words.Count == 0) + return Result.Ok(words); + + return Result.Ok(words.Where(w => w != null).ToList()); + } + + public bool ShouldInclude(string word) + { + return word != null; + } + } +} diff --git a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs index b6745ce03..9d0170d84 100644 --- a/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs +++ b/TagCloudGenerator/Infrastructure/Normalizers/LowerCaseNormalizer.cs @@ -6,8 +6,8 @@ public class LowerCaseNormalizer : INormalizer { public Result> Normalize(List words) { - if (words == null) - return Result.Fail>("Can't normalize if list is null"); + if (words == null || words.Count == 0) + return Result.Ok(words); return Result.Of(() => words.Select(w => w.ToLower()).ToList()); } diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 0d19ed2c9..ba320a9c9 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -13,14 +13,14 @@ public bool CanRead(string filePath) public Result> TryRead(string filePath) { - var docResult = Result.Of(() => - WordprocessingDocument.Open(filePath, false), - $"Failed to get document: '{filePath}'"); + var docOpeningResult = Result.Of(() => + WordprocessingDocument.Open(filePath, false), + $"Failed to get document: '{filePath}'"); - if (!docResult.IsSuccess) - return Result.Fail>(docResult.Error); + if (!docOpeningResult.IsSuccess) + return Result.Fail>(docOpeningResult.Error); - var doc = docResult.Value; + var doc = docOpeningResult.Value; if (doc.MainDocumentPart == null) return Result.Fail>("Missing MainDocumentPart"); diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index 1558b0141..e5f7f9d5b 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -18,19 +18,19 @@ public Result Render(IEnumerable items, CanvasSettings canvas return Result.Fail("Invalid canvas size"); var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); - using (Graphics graphics = Graphics.FromImage(bitmap)) + using (var graphics = Graphics.FromImage(bitmap)) { ConfigureGraphics(graphics); graphics.Clear(canvasSettings.BackgroundColor); var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); - using (SolidBrush brush = new SolidBrush(textSettings.TextColor)) + using (var brush = new SolidBrush(textSettings.TextColor)) { - using (StringFormat stringFormat = new StringFormat()) + using (var stringFormat = new StringFormat()) { stringFormat.Alignment = StringAlignment.Center; stringFormat.LineAlignment = StringAlignment.Center; - using (Pen pen = new Pen(textSettings.TextColor, 1)) + using (var pen = new Pen(textSettings.TextColor, 1)) { pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; diff --git a/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs b/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs index 6911fa252..d0ccf945d 100644 --- a/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs +++ b/TagCloudGenerator/Infrastructure/Sorters/FrequencyDescendingSorter.cs @@ -5,31 +5,14 @@ namespace TagCloudGenerator.Infrastructure.Sorterers { public class FrequencyDescendingSorter : ISorter { - public Result Sort(Dictionary wordsWithFreqs) + public Result Sort(WordsWithFrequency words) { - if (wordsWithFreqs == null) - return Result.Fail("Found no words to sort"); + if (words == null || words.WordsWithFreq.Count == 0) + return Result.Ok(words); - var minFreq = int.MaxValue; - var maxFreq = int.MinValue; + var sorted = words.WordsWithFreq.OrderByDescending(w => w.Frequency).ThenBy(w => w.Word).ToList(); - var words = new List(); - - foreach (var word in wordsWithFreqs) - { - if(word.Key == null) - return Result.Fail("Sorter found null key in dictionary"); - - var wordWithFreq = new WordFrequencyData { Word = word.Key, Frequency = word.Value }; - minFreq = Math.Min(minFreq, word.Value); - maxFreq = Math.Max(maxFreq, word.Value); - - words.Add(wordWithFreq); - } - - var sorted = words.OrderByDescending(w => w.Frequency).ThenBy(w => w.Word).ToList(); - - var result = Result.Ok(new WordsWithFrequency { WordsWithFreq = sorted, MaxFreq = maxFreq, MinFreq = minFreq }); + var result = Result.Ok(new WordsWithFrequency { WordsWithFreq = sorted, MaxFreq = words.MaxFreq, MinFreq = words.MinFreq }); return result; } } From db85b57b28b5a6cc9649b04ef8ecf04c5895fb49 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 10 Jan 2026 23:17:19 +0500 Subject: [PATCH 08/13] Moved renderer to console client --- TagCloudConsoleClient/ConsoleClient.cs | 7 +++++-- TagCloudConsoleClient/Program.cs | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/TagCloudConsoleClient/ConsoleClient.cs b/TagCloudConsoleClient/ConsoleClient.cs index 5f0ef8f0b..a86c450ae 100644 --- a/TagCloudConsoleClient/ConsoleClient.cs +++ b/TagCloudConsoleClient/ConsoleClient.cs @@ -10,16 +10,18 @@ namespace TagCloudConsoleClient public class ConsoleClient : IClient { private readonly ITagCloudGenerator _generator; - private readonly IEnumerable _filters; + private readonly ICollection _filters; private readonly IReaderRepository _readersRepository; private readonly INormalizer _normalizer; + private readonly IRenderer _renderer; - public ConsoleClient(ITagCloudGenerator generator, IEnumerable filters, IReaderRepository readers, INormalizer normalizer) + public ConsoleClient(ITagCloudGenerator generator, ICollection filters, IReaderRepository readers, INormalizer normalizer, IRenderer renderer) { _generator = generator; _filters = filters; _readersRepository = readers; _normalizer = normalizer; + _renderer = renderer; } public void Run(string[] args) @@ -60,6 +62,7 @@ private void RunWithOptions(Options opts) .Then(reader => reader.TryRead(inputFile)) .Then(words => _normalizer.Normalize(words)) .Then(words => _generator.Generate(words, canvasSettings, textSettings, _filters)) + .Then(items => _renderer.Render(items, canvasSettings, textSettings)) .Then(image => Result.OfAction(() => { image.Save(outputFile, ImageFormat.Png); diff --git a/TagCloudConsoleClient/Program.cs b/TagCloudConsoleClient/Program.cs index 4944c8f91..be4fbe3fb 100644 --- a/TagCloudConsoleClient/Program.cs +++ b/TagCloudConsoleClient/Program.cs @@ -12,6 +12,7 @@ public static void Main(string[] args) var builder = new ContainerBuilder(); builder.RegisterModule(); + builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); From a28b2b67a85071b12ee1df42d20f4920c02ed4c2 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 10 Jan 2026 23:17:35 +0500 Subject: [PATCH 09/13] Moved renderer to UI client --- TagCloudUIClient/MainForm.Designer.cs | 15 +++++++++------ TagCloudUIClient/Program.cs | 3 +++ TagCloudUIClient/WinFormsClient.cs | 6 ++++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/TagCloudUIClient/MainForm.Designer.cs b/TagCloudUIClient/MainForm.Designer.cs index 3cdd20cf6..83223728f 100644 --- a/TagCloudUIClient/MainForm.Designer.cs +++ b/TagCloudUIClient/MainForm.Designer.cs @@ -18,6 +18,7 @@ public partial class MainForm : Form private readonly ITagCloudGenerator _generator; private readonly IReaderRepository _readersRepository; private readonly INormalizer _normalizer; + private readonly IRenderer _renderer; private string _lastOutputPath = string.Empty; private List wordsToRender; @@ -45,11 +46,12 @@ public partial class MainForm : Form private Button btnSave; - public MainForm(ITagCloudGenerator generator, IReaderRepository readers, INormalizer normalizer) + public MainForm(ITagCloudGenerator generator, IReaderRepository readers, INormalizer normalizer, IRenderer renderer) { _generator = generator; _readersRepository = readers; _normalizer = normalizer; + _renderer = renderer; InitializeComponent(); } @@ -134,8 +136,6 @@ private void btnGenerate_Click(object sender, EventArgs e) .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; @@ -151,8 +151,11 @@ private void btnGenerate_Click(object sender, EventArgs e) .SetMinFontSize((int)numMinSize.Value) .SetTextColor(textColor); + var filters = new List() { new NullWordsFilter(), new BoringWordsFilter(excluded) }; + _generator.Generate(wordsToRender, canvasSettings, textSettings, filters) - .OnSuccess(bitmap => + .Then(items => _renderer.Render(items, canvasSettings, textSettings)) + .Then(bitmap => { picturePreview.Image?.Dispose(); _generatedBitmap = bitmap; @@ -161,8 +164,8 @@ private void btnGenerate_Click(object sender, EventArgs e) }) .OnFail(error => { MessageBox.Show( - $"Generation error: {error}", - "Generation was not successful", + $"Rendering error: {error}", + "Rendering was not successful", MessageBoxButtons.OK, MessageBoxIcon.Error); }); diff --git a/TagCloudUIClient/Program.cs b/TagCloudUIClient/Program.cs index 650b4211a..7a500d82f 100644 --- a/TagCloudUIClient/Program.cs +++ b/TagCloudUIClient/Program.cs @@ -1,6 +1,7 @@ using Autofac; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.DI; +using TagCloudGenerator.Infrastructure.Filters; namespace TagCloudUIClient { @@ -18,6 +19,8 @@ static void Main() var builder = new ContainerBuilder(); builder.RegisterModule(); + builder.RegisterType().As(); + builder.RegisterType().As(); builder.RegisterType().As().SingleInstance(); var container = builder.Build(); diff --git a/TagCloudUIClient/WinFormsClient.cs b/TagCloudUIClient/WinFormsClient.cs index c0ffe1b54..7faf03287 100644 --- a/TagCloudUIClient/WinFormsClient.cs +++ b/TagCloudUIClient/WinFormsClient.cs @@ -7,12 +7,14 @@ public class WinFormsClient : IClient private readonly ITagCloudGenerator _generator; private readonly IReaderRepository _reader; private readonly INormalizer _normalizer; + private readonly IRenderer _renderer; - public WinFormsClient(ITagCloudGenerator generator, IReaderRepository repository, INormalizer normalizer) + public WinFormsClient(ITagCloudGenerator generator, IReaderRepository repository, INormalizer normalizer, IRenderer renderer) { _generator = generator; _reader = repository; _normalizer = normalizer; + _renderer = renderer; } public void Run(string[] args) @@ -20,7 +22,7 @@ public void Run(string[] args) Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); - var mainForm = new MainForm(_generator, _reader, _normalizer); + var mainForm = new MainForm(_generator, _reader, _normalizer, _renderer); Application.Run(mainForm); } } From ca24a761205553b09b0c51c086ea480043d600bf Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sat, 10 Jan 2026 23:18:03 +0500 Subject: [PATCH 10/13] Updated tests --- TagCloudGeneratorTests/CloudGeneratorTests.cs | 23 +------- .../WordsFrequencyAnalyzerTests.cs | 58 ++++++++++++++----- 2 files changed, 45 insertions(+), 36 deletions(-) diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index d39c7cd5f..1cda49857 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -93,9 +93,7 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() var words = new List { "hello", "world", "hello" }; var filteredWords = new List { "hello", "world", "hello" }; - var analyzed = new Dictionary { { "hello", 2 }, { "world", 1 } }; - - var sorted = new WordsWithFrequency + var analyzed = new WordsWithFrequency { WordsWithFreq = new List { @@ -106,6 +104,8 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() MinFreq = 1 }; + var sorted = analyzed; + filterMock .Setup(f => f.ShouldInclude(It.IsAny())) .Returns(true); @@ -142,13 +142,6 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() .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(), @@ -156,21 +149,11 @@ public void Generate_NormalFlow_CallsAllDependencies_Test() new[] { filterMock.Object }); Assert.That(result.IsSuccess, Is.True); - result.GetValueOrThrow().Dispose(); filterMock.Verify(f => f.ShouldInclude(It.IsAny()), Times.Exactly(3)); analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); algorithmMock.Verify(a => a.Reset(), Times.Once); algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Exactly(2)); - - rendererMock.Verify(r => r.Render( - It.Is>(items => items.Count() == 2), - It.IsAny(), - It.Is(ts => - ts.FontFamily == "Arial" && - Math.Abs(ts.MinFontSize - 12) < float.Epsilon && - Math.Abs(ts.MaxFontSize - 72) < float.Epsilon)), - Times.Once); } } diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index 8fdc1cd9d..e930497e1 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -12,41 +12,67 @@ public void Analyze_EmptyInput_ReturnsEmpty_Test() { var words = new List(); var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); - Assert.That(result, Is.Empty); + Assert.That(result.WordsWithFreq, Is.Empty); } [Test] public void Analyze_SingleWord_ReturnsOneItemWithFrequencyOne_Test() { var words = new List { "hello" }; - var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); + + var wordsWithFrequency = result.WordsWithFreq; - Assert.That(result.Count, Is.EqualTo(1)); - Assert.That(result[0].Key, Is.EqualTo("hello")); - Assert.That(result[0].Value, Is.EqualTo(1)); + Assert.That(wordsWithFrequency.Count, Is.EqualTo(1)); + Assert.That(wordsWithFrequency[0].Word, Is.EqualTo("hello")); + Assert.That(wordsWithFrequency[0].Frequency, Is.EqualTo(1)); } [Test] public void Analyze_MultipleWords_ReturnsCorrectFrequencies_Test() { var words = new List { "hello", "world", "hello", "test", "world", "hello" }; - var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); - - Assert.That(result.Count, Is.EqualTo(3)); - 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)); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); + + var wordsWithFrequency = result.WordsWithFreq; + + Assert.That(wordsWithFrequency.Count, Is.EqualTo(3)); + Assert.That(wordsWithFrequency[0].Word, Is.EqualTo("hello")); + Assert.That(wordsWithFrequency[0].Frequency, Is.EqualTo(3)); + Assert.That(wordsWithFrequency[1].Word, Is.EqualTo("world")); + Assert.That(wordsWithFrequency[1].Frequency, Is.EqualTo(2)); + Assert.That(wordsWithFrequency[2].Word, Is.EqualTo("test")); + Assert.That(wordsWithFrequency[2].Frequency, Is.EqualTo(1)); } [Test] public void Analyze_CaseSensitive_ReturnsSeparateItems_Test() { var words = new List { "Hello", "hello", "HELLO" }; - var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow().ToList(); - Assert.That(result.Count, Is.EqualTo(3)); + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); + + var wordsWithFrequency = result.WordsWithFreq; + Assert.That(wordsWithFrequency.Count, Is.EqualTo(3)); + } + + [Test] + public void Analyze_MultipleWords_ReturnsExpectedFrequencyLimits_Test() + { + var words = new List { "hello", "world", "hello", "test", "world", "hello" }; + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); + + Assert.That(result.MaxFreq, Is.EqualTo(3)); + Assert.That(result.MinFreq, Is.EqualTo(1)); + } + + [Test] + public void Analyze_NoWords_StoresIntegerLimits_Test() + { + var words = new List { }; + var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); + + Assert.That(result.MaxFreq, Is.EqualTo(int.MinValue)); + Assert.That(result.MinFreq, Is.EqualTo(int.MaxValue)); } } } \ No newline at end of file From 264f1a55103a6c373f61184acf07d581f6d5d9b5 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sun, 11 Jan 2026 21:00:50 +0500 Subject: [PATCH 11/13] Updated generator, docxReader, renderer and analyzer --- .../Core/Services/CloudGenerator.cs | 8 ++--- .../Analyzers/WordsFrequencyAnalyzer.cs | 4 +-- .../Infrastructure/Readers/DocxReader.cs | 36 +++++++++---------- .../Infrastructure/Renderers/PngRenderer.cs | 33 +++++++++-------- 4 files changed, 40 insertions(+), 41 deletions(-) diff --git a/TagCloudGenerator/Core/Services/CloudGenerator.cs b/TagCloudGenerator/Core/Services/CloudGenerator.cs index f8821e286..0e08f8474 100644 --- a/TagCloudGenerator/Core/Services/CloudGenerator.cs +++ b/TagCloudGenerator/Core/Services/CloudGenerator.cs @@ -36,12 +36,12 @@ public Result> Generate(List words, CanvasSettings canva var filteredWords = ApplyFilters(words, filters); var wordsWithFreq = _analyzer.Analyze(filteredWords); - if (!wordsWithFreq.IsSuccess || wordsWithFreq.Value == null) - return Result.Fail>("Analyzed words collection is null"); + if (!wordsWithFreq.IsSuccess) + return Result.Fail>(wordsWithFreq.Error); var sortedWords = _sorter.Sort(wordsWithFreq.Value); - if (!sortedWords.IsSuccess || sortedWords.Value == null) - return Result.Fail>("Sorted words collection is null"); + if (!sortedWords.IsSuccess) + return Result.Fail>(sortedWords.Error); return InitializeCloudItems(sortedWords.Value, textSettings).ToList(); } diff --git a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs index 9912fbda3..4f5b5250f 100644 --- a/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs +++ b/TagCloudGenerator/Infrastructure/Analyzers/WordsFrequencyAnalyzer.cs @@ -17,8 +17,8 @@ public Result Analyze(List words) return Result.Ok(new WordsWithFrequency { WordsWithFreq = wordFreqList, - MaxFreq = maxFrequency, - MinFreq = minFrequency + MaxFreq = 0, + MinFreq = 0 }); foreach (string word in words) diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index ba320a9c9..1947fcb95 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -20,27 +20,23 @@ public Result> TryRead(string filePath) if (!docOpeningResult.IsSuccess) return Result.Fail>(docOpeningResult.Error); - var doc = docOpeningResult.Value; - - if (doc.MainDocumentPart == null) - return Result.Fail>("Missing MainDocumentPart"); - - if (doc.MainDocumentPart.Document.Body == null) - return Result.Fail>("Missing document body"); - - var result = Result.Of(() => + return Result.Of(() => { - return doc.MainDocumentPart.Document.Body - .Elements() - .Select(p => p.InnerText) - .Where(t => !string.IsNullOrWhiteSpace(t)) - .ToList(); - }, - $"Failed to load .docx document: '{filePath}'"); - - doc.Dispose(); - - return result; + using (var doc = WordprocessingDocument.Open(filePath, false)) + { + if (doc.MainDocumentPart == null) + throw new InvalidOperationException($"Document has no main part: '{filePath}'"); + + if (doc.MainDocumentPart.Document?.Body == null) + throw new InvalidOperationException($"Document has no body content: '{filePath}'"); + + return doc.MainDocumentPart.Document.Body + .Elements() + .Select(p => p.InnerText) + .Where(t => !string.IsNullOrWhiteSpace(t)) + .ToList(); + } + }, "Failed to read DOCX file"); } } } diff --git a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs index e5f7f9d5b..e0917b59d 100644 --- a/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs +++ b/TagCloudGenerator/Infrastructure/Renderers/PngRenderer.cs @@ -9,7 +9,7 @@ public class PngRenderer : IRenderer public Result Render(IEnumerable items, CanvasSettings canvasSettings, TextSettings textSettings) { if (items == null) - return Result.Fail("Cloud items are null"); + items = new List(); var itemsList = items.ToList(); @@ -18,32 +18,35 @@ public Result Render(IEnumerable items, CanvasSettings canvas return Result.Fail("Invalid canvas size"); var bitmap = new Bitmap(canvasSettings.CanvasSize.Width, canvasSettings.CanvasSize.Height); - using (var graphics = Graphics.FromImage(bitmap)) + + var result = Result.Of(() => { - ConfigureGraphics(graphics); - graphics.Clear(canvasSettings.BackgroundColor); - var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); - using (var brush = new SolidBrush(textSettings.TextColor)) + using (var graphics = Graphics.FromImage(bitmap)) { - using (var stringFormat = new StringFormat()) - { - stringFormat.Alignment = StringAlignment.Center; - stringFormat.LineAlignment = StringAlignment.Center; + ConfigureGraphics(graphics); + graphics.Clear(canvasSettings.BackgroundColor); + var (offsetX, offsetY) = CalculateOffset(itemsList, canvasSettings); - using (var pen = new Pen(textSettings.TextColor, 1)) + using (var brush = new SolidBrush(textSettings.TextColor)) + { + using (var stringFormat = new StringFormat()) { - pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; + stringFormat.Alignment = StringAlignment.Center; + stringFormat.LineAlignment = StringAlignment.Center; - return Result.Of(() => + using (var pen = new Pen(textSettings.TextColor, 1)) { + pen.DashStyle = System.Drawing.Drawing2D.DashStyle.Dash; foreach (var item in itemsList) DrawCloudItem(graphics, item, offsetX, offsetY, canvasSettings, textSettings, brush, pen, stringFormat); return bitmap; - }); + } } } } - } + }); + + return result; } private (int offsetX, int offsetY) CalculateOffset( From 8a4bf9bb839bfb3b11a5f01d0fffa7067ce7a4b7 Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sun, 11 Jan 2026 21:01:22 +0500 Subject: [PATCH 12/13] Updated generator and analyzer tests --- TagCloudGeneratorTests/CloudGeneratorTests.cs | 77 ++++++++++++++----- .../WordsFrequencyAnalyzerTests.cs | 6 +- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/TagCloudGeneratorTests/CloudGeneratorTests.cs b/TagCloudGeneratorTests/CloudGeneratorTests.cs index 1cda49857..28f5c356f 100644 --- a/TagCloudGeneratorTests/CloudGeneratorTests.cs +++ b/TagCloudGeneratorTests/CloudGeneratorTests.cs @@ -1,6 +1,7 @@ using Moq; using NUnit.Framework; using System.Drawing; +using System.Globalization; using TagCloudGenerator.Core.Interfaces; using TagCloudGenerator.Core.Models; using TagCloudGenerator.Core.Services; @@ -40,51 +41,89 @@ public void Setup() } [Test] - public void Generate_EmptyWords_ReturnsFailResult_AndDoesNotRender_Test() + public void Generate_EmptyWords_ReturnsSuccessfulResult_Test() { + var words = new List { }; + var filteredWords = new List { }; + + var analyzed = new WordsWithFrequency + { + WordsWithFreq = new List { }, + MaxFreq = 0, + MinFreq = 0 + }; + + var sorted = analyzed; + filterMock - .Setup(f => f.Filter(It.IsAny>())) - .Returns(new List()); + .Setup(f => f.ShouldInclude(It.IsAny())) + .Returns(true); + + analyzerMock + .Setup(a => a.Analyze(filteredWords)) + .Returns(analyzed); + + sortererMock + .Setup(s => s.Sort(analyzed)) + .Returns(Result.Ok(sorted)); + + var textSettings = new TextSettings() + .SetFontFamily("Arial") + .SetFontSizeRange(12, 72); var result = cloudGenerator.Generate( - new List(), + words, new CanvasSettings(), - new TextSettings(), + textSettings, new[] { filterMock.Object }); - Assert.That(result.IsSuccess, Is.False); + Assert.That(result.IsSuccess, Is.True); - rendererMock.Verify(r => r.Render( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Never); + filterMock.Verify(f => f.ShouldInclude(It.IsAny()), Times.Never); + analyzerMock.Verify(a => a.Analyze(filteredWords), Times.Once); + algorithmMock.Verify(a => a.Reset(), Times.Once); + algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Never); } [Test] - public void Generate_AllWordsFilteredOut_ReturnsFailResult_Test() + public void Generate_AllWordsFilteredOut_GeneratesSuccessfuly_Test() { var words = new List { "in", "a", "for" }; var filtered = new List(); + var analyzed = new WordsWithFrequency + { + WordsWithFreq = new List { }, + MaxFreq = 0, + MinFreq = 0 + }; + + var sorted = analyzed; + filterMock .Setup(f => f.Filter(words)) .Returns(filtered); + analyzerMock + .Setup(a => a.Analyze(filtered)) + .Returns(analyzed); + + sortererMock + .Setup(s => s.Sort(analyzed)) + .Returns(Result.Ok(sorted)); + var result = cloudGenerator.Generate( words, new CanvasSettings(), new TextSettings(), new[] { filterMock.Object }); - Assert.That(result.IsSuccess, Is.False); - - rendererMock.Verify(r => r.Render( - It.IsAny>(), - It.IsAny(), - It.IsAny()), - Times.Never); + Assert.That(result.IsSuccess, Is.True); + filterMock.Verify(f => f.ShouldInclude(It.IsAny()), Times.Exactly(3)); + analyzerMock.Verify(a => a.Analyze(filtered), Times.Once); + algorithmMock.Verify(a => a.Reset(), Times.Once); + algorithmMock.Verify(a => a.PutNextRectangle(It.IsAny()), Times.Never); } [Test] diff --git a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs index e930497e1..424c60241 100644 --- a/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs +++ b/TagCloudGeneratorTests/WordsFrequencyAnalyzerTests.cs @@ -66,13 +66,13 @@ public void Analyze_MultipleWords_ReturnsExpectedFrequencyLimits_Test() } [Test] - public void Analyze_NoWords_StoresIntegerLimits_Test() + public void Analyze_NoWords_StoresValueOfZero_Test() { var words = new List { }; var result = frequencyAnalyzer.Analyze(words).GetValueOrThrow(); - Assert.That(result.MaxFreq, Is.EqualTo(int.MinValue)); - Assert.That(result.MinFreq, Is.EqualTo(int.MaxValue)); + Assert.That(result.MaxFreq, Is.EqualTo(0)); + Assert.That(result.MinFreq, Is.EqualTo(0)); } } } \ No newline at end of file From 239700a5a56b4a8fbd5e481bed73729bfe038c5f Mon Sep 17 00:00:00 2001 From: Anton Savitskikh Date: Sun, 11 Jan 2026 21:24:02 +0500 Subject: [PATCH 13/13] Updated DocxReader --- .../Infrastructure/Readers/DocxReader.cs | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs index 1947fcb95..2266c2f60 100644 --- a/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs +++ b/TagCloudGenerator/Infrastructure/Readers/DocxReader.cs @@ -13,28 +13,20 @@ public bool CanRead(string filePath) public Result> TryRead(string filePath) { - var docOpeningResult = Result.Of(() => - WordprocessingDocument.Open(filePath, false), - $"Failed to get document: '{filePath}'"); - - if (!docOpeningResult.IsSuccess) - return Result.Fail>(docOpeningResult.Error); - return Result.Of(() => { - using (var doc = WordprocessingDocument.Open(filePath, false)) + using (var doc = WordprocessingDocument.Open(filePath, true)) { - if (doc.MainDocumentPart == null) - throw new InvalidOperationException($"Document has no main part: '{filePath}'"); - - if (doc.MainDocumentPart.Document?.Body == null) - throw new InvalidOperationException($"Document has no body content: '{filePath}'"); - - return doc.MainDocumentPart.Document.Body + if(doc.MainDocumentPart != null && doc.MainDocumentPart.Document.Body != null) + { + return doc.MainDocumentPart.Document.Body .Elements() .Select(p => p.InnerText) .Where(t => !string.IsNullOrWhiteSpace(t)) .ToList(); + } + + return new List(); } }, "Failed to read DOCX file"); }