From da6877cb5caf058b79e3797a811630eb13afc7bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B0=D0=B2=D0=B5=D0=BB=D0=B8=D0=B9?= Date: Tue, 21 Jan 2025 22:44:20 +0500 Subject: [PATCH] Add Result pattern --- TagCloud2/CloudForms/CircularSpiral.cs | 17 +++ TagCloud2/CloudForms/Direction.cs | 9 ++ TagCloud2/CloudForms/DirectionExtension.cs | 16 +++ TagCloud2/CloudForms/ICloudForm.cs | 8 ++ TagCloud2/CloudForms/SquareSpiral.cs | 42 ++++++ .../CloudGenerator/IRectanglesGenerator.cs | 8 ++ .../CloudGenerator/RectanglesGenerator.cs | 50 +++++++ TagCloud2/CloudGenerator/WordInShape.cs | 5 + TagCloud2/CloudLayout/CloudExtension.cs | 17 +++ TagCloud2/CloudLayout/CloudLayouter.cs | 24 ++++ TagCloud2/CloudLayout/ICloudLayouter.cs | 8 ++ TagCloud2/ColoringAlgorithms/GradientColor.cs | 23 ++++ .../ColoringAlgorithms/IColorAlgorithm.cs | 9 ++ TagCloud2/ColoringAlgorithms/RandomColor.cs | 19 +++ TagCloud2/ColoringAlgorithms/SingleColor.cs | 14 ++ TagCloud2/Drawer/DrawerSettings.cs | 6 + TagCloud2/Drawer/ICloudDrawer.cs | 8 ++ TagCloud2/Drawer/RectanglesCloudDrawer.cs | 68 +++++++++ TagCloud2/FileReader/IFileReader.cs | 8 ++ TagCloud2/FileReader/TxtReader.cs | 28 ++++ TagCloud2/Result.cs | 130 ++++++++++++++++++ TagCloud2/SaviorImages.cs | 12 ++ TagCloud2/TagCloud2.csproj | 16 +++ TagCloud2/TagCloudApp/Options.cs | 24 ++++ TagCloud2/TagCloudApp/Program.cs | 89 ++++++++++++ TagCloud2/TagCloudApp/TagCloudAlgorithms.cs | 7 + TagCloud2/TextPreparator/ITextFilter.cs | 8 ++ TagCloud2/TextPreparator/IWordsFrequency.cs | 8 ++ TagCloud2/TextPreparator/TextFilter.cs | 22 +++ TagCloud2/TextPreparator/TextHandler.cs | 27 ++++ TagCloud2Tests/CloudGeneratorTests.cs | 76 ++++++++++ TagCloud2Tests/CloudLayoutTests.cs | 120 ++++++++++++++++ TagCloud2Tests/ColoringAlgorithmsTests.cs | 72 ++++++++++ TagCloud2Tests/TagCloud2Tests.csproj | 21 +++ TagCloud2Tests/TextPreparatorTests.cs | 126 +++++++++++++++++ 35 files changed, 1145 insertions(+) create mode 100644 TagCloud2/CloudForms/CircularSpiral.cs create mode 100644 TagCloud2/CloudForms/Direction.cs create mode 100644 TagCloud2/CloudForms/DirectionExtension.cs create mode 100644 TagCloud2/CloudForms/ICloudForm.cs create mode 100644 TagCloud2/CloudForms/SquareSpiral.cs create mode 100644 TagCloud2/CloudGenerator/IRectanglesGenerator.cs create mode 100644 TagCloud2/CloudGenerator/RectanglesGenerator.cs create mode 100644 TagCloud2/CloudGenerator/WordInShape.cs create mode 100644 TagCloud2/CloudLayout/CloudExtension.cs create mode 100644 TagCloud2/CloudLayout/CloudLayouter.cs create mode 100644 TagCloud2/CloudLayout/ICloudLayouter.cs create mode 100644 TagCloud2/ColoringAlgorithms/GradientColor.cs create mode 100644 TagCloud2/ColoringAlgorithms/IColorAlgorithm.cs create mode 100644 TagCloud2/ColoringAlgorithms/RandomColor.cs create mode 100644 TagCloud2/ColoringAlgorithms/SingleColor.cs create mode 100644 TagCloud2/Drawer/DrawerSettings.cs create mode 100644 TagCloud2/Drawer/ICloudDrawer.cs create mode 100644 TagCloud2/Drawer/RectanglesCloudDrawer.cs create mode 100644 TagCloud2/FileReader/IFileReader.cs create mode 100644 TagCloud2/FileReader/TxtReader.cs create mode 100644 TagCloud2/Result.cs create mode 100644 TagCloud2/SaviorImages.cs create mode 100644 TagCloud2/TagCloud2.csproj create mode 100644 TagCloud2/TagCloudApp/Options.cs create mode 100644 TagCloud2/TagCloudApp/Program.cs create mode 100644 TagCloud2/TagCloudApp/TagCloudAlgorithms.cs create mode 100644 TagCloud2/TextPreparator/ITextFilter.cs create mode 100644 TagCloud2/TextPreparator/IWordsFrequency.cs create mode 100644 TagCloud2/TextPreparator/TextFilter.cs create mode 100644 TagCloud2/TextPreparator/TextHandler.cs create mode 100644 TagCloud2Tests/CloudGeneratorTests.cs create mode 100644 TagCloud2Tests/CloudLayoutTests.cs create mode 100644 TagCloud2Tests/ColoringAlgorithmsTests.cs create mode 100644 TagCloud2Tests/TagCloud2Tests.csproj create mode 100644 TagCloud2Tests/TextPreparatorTests.cs diff --git a/TagCloud2/CloudForms/CircularSpiral.cs b/TagCloud2/CloudForms/CircularSpiral.cs new file mode 100644 index 000000000..9097e7b19 --- /dev/null +++ b/TagCloud2/CloudForms/CircularSpiral.cs @@ -0,0 +1,17 @@ +using System.Drawing; + +namespace TagCloud2.CloudForms; + +public class CircularSpiral(Point center) : ICloudForm +{ + private double _angle; + private const double AngleStep = 0.01; + + public Point GetNextPoint() + { + _angle += AngleStep; + var x = (int) (center.X + _angle * Math.Cos(_angle)); + var y = (int) (center.Y + _angle * Math.Sin(_angle)); + return new Point(x, y); + } +} \ No newline at end of file diff --git a/TagCloud2/CloudForms/Direction.cs b/TagCloud2/CloudForms/Direction.cs new file mode 100644 index 000000000..910db5183 --- /dev/null +++ b/TagCloud2/CloudForms/Direction.cs @@ -0,0 +1,9 @@ +namespace TagCloud2.CloudForms; + +public enum Direction +{ + Up, + Right, + Down, + Left, +} \ No newline at end of file diff --git a/TagCloud2/CloudForms/DirectionExtension.cs b/TagCloud2/CloudForms/DirectionExtension.cs new file mode 100644 index 000000000..c2119b97d --- /dev/null +++ b/TagCloud2/CloudForms/DirectionExtension.cs @@ -0,0 +1,16 @@ +namespace TagCloud2.CloudForms; + +public static class DirectionExtension +{ + public static Direction ClockwiseRotate(this Direction direction) + { + return direction switch + { + Direction.Up => Direction.Right, + Direction.Right => Direction.Down, + Direction.Down => Direction.Left, + Direction.Left => Direction.Up, + _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) + }; + } +} \ No newline at end of file diff --git a/TagCloud2/CloudForms/ICloudForm.cs b/TagCloud2/CloudForms/ICloudForm.cs new file mode 100644 index 000000000..0709632e9 --- /dev/null +++ b/TagCloud2/CloudForms/ICloudForm.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud2.CloudForms; + +public interface ICloudForm +{ + public Point GetNextPoint(); +} \ No newline at end of file diff --git a/TagCloud2/CloudForms/SquareSpiral.cs b/TagCloud2/CloudForms/SquareSpiral.cs new file mode 100644 index 000000000..9ebb5c765 --- /dev/null +++ b/TagCloud2/CloudForms/SquareSpiral.cs @@ -0,0 +1,42 @@ +using System.Drawing; + +namespace TagCloud2.CloudForms; + +public class SquareSpiral(Point centre) : ICloudForm +{ + private const int Step = 1; + private int _stepCounter = 1; + private int _neededSteps = 1; + private Direction _direction = Direction.Up; + private Point _previus = centre; + + public Point GetNextPoint() + { + _previus += GetOffsetSize(_direction); + + _stepCounter--; + + if (_stepCounter == 0) + { + _direction = _direction.ClockwiseRotate(); + + if (_direction == Direction.Up || _direction == Direction.Down) + { + _neededSteps++; + } + + _stepCounter = _neededSteps; + } + + return _previus; + } + + private Size GetOffsetSize(Direction direction) => direction switch + { + Direction.Up => new Size(0, Step), + Direction.Right => new Size(Step, 0), + Direction.Down => new Size(0, -Step), + Direction.Left => new Size(-Step, 0), + _ => throw new ArgumentOutOfRangeException(nameof(direction)) + }; +} \ No newline at end of file diff --git a/TagCloud2/CloudGenerator/IRectanglesGenerator.cs b/TagCloud2/CloudGenerator/IRectanglesGenerator.cs new file mode 100644 index 000000000..2043dcf68 --- /dev/null +++ b/TagCloud2/CloudGenerator/IRectanglesGenerator.cs @@ -0,0 +1,8 @@ +using TagCloud; + +namespace TagCloud2.CloudGenerator; + +public interface IRectanglesGenerator +{ + public Result> GetWordsInShape(IDictionary wordToWeight); +} \ No newline at end of file diff --git a/TagCloud2/CloudGenerator/RectanglesGenerator.cs b/TagCloud2/CloudGenerator/RectanglesGenerator.cs new file mode 100644 index 000000000..828c2bbb0 --- /dev/null +++ b/TagCloud2/CloudGenerator/RectanglesGenerator.cs @@ -0,0 +1,50 @@ +using System.Drawing; +using TagCloud; +using TagCloud2.CloudLayout; +using TagCloud2.Drawer; + +namespace TagCloud2.CloudGenerator; + +public class RectanglesGenerator(ICloudLayouter cloudLayouter, DrawerSettings drawerSettings) : IRectanglesGenerator +{ + private readonly List _wordsInShape = new(); + private const int MinRectangleWidth = 5; + private const int MinRectangleHeight = 5; + + + public Result> GetWordsInShape(IDictionary wordToWeight) + { + foreach (var word in wordToWeight) + { + var current = word.Key; + var size = GenerateRectangleSize(word, wordToWeight.Count); + var rectangle = cloudLayouter.PutNextRectangle(size); + if (rectangle.Width / current.Length < 1) + { + return Result.Fail>("Too small cloud size, try to take greater parameter"); + } + + var fontSize = GenerateFontSize(rectangle, current); + _wordsInShape.Add(new WordInShape(current, rectangle, fontSize)); + } + + return _wordsInShape; + } + + private float GenerateFontSize(Rectangle rectangle, string word) + { + var fontSizeWidth = rectangle.Width / word.Length; + var fontSizeHeight = rectangle.Height; + return Math.Min(fontSizeWidth, fontSizeHeight); + } + + private Size GenerateRectangleSize(KeyValuePair word, int count) + { + var length = word.Key.Length; + var value = word.Value; + var width = length * value * drawerSettings.CloudSize.Width / count; + var height = value * drawerSettings.CloudSize.Height / count; + return new Size(Math.Max(width, MinRectangleWidth), + Math.Max(height, MinRectangleHeight)); + } +} \ No newline at end of file diff --git a/TagCloud2/CloudGenerator/WordInShape.cs b/TagCloud2/CloudGenerator/WordInShape.cs new file mode 100644 index 000000000..7f025e39a --- /dev/null +++ b/TagCloud2/CloudGenerator/WordInShape.cs @@ -0,0 +1,5 @@ +using System.Drawing; + +namespace TagCloud2.CloudGenerator; + +public record WordInShape(string Word, Rectangle Rectangle, float FontSize); \ No newline at end of file diff --git a/TagCloud2/CloudLayout/CloudExtension.cs b/TagCloud2/CloudLayout/CloudExtension.cs new file mode 100644 index 000000000..20e392adc --- /dev/null +++ b/TagCloud2/CloudLayout/CloudExtension.cs @@ -0,0 +1,17 @@ +using System.Drawing; +using TagCloud2.CloudGenerator; + +namespace TagCloud2.CloudLayout; + +public static class CloudExtension +{ + public static Size GetCloudSize(this IList words) + { + var left = words.Min(x => x.Rectangle.Left); + var right = words.Max(x => x.Rectangle.Right); + var top = words.Min(x => x.Rectangle.Top); + var bottom = words.Max(x => x.Rectangle.Bottom); + var size = new Size( Math.Abs(right) + Math.Abs(left),Math.Abs(bottom) + Math.Abs(top)); + return size; + } +} \ No newline at end of file diff --git a/TagCloud2/CloudLayout/CloudLayouter.cs b/TagCloud2/CloudLayout/CloudLayouter.cs new file mode 100644 index 000000000..22f2f2f4b --- /dev/null +++ b/TagCloud2/CloudLayout/CloudLayouter.cs @@ -0,0 +1,24 @@ +using System.Drawing; +using TagCloud2.CloudForms; + +namespace TagCloud2.CloudLayout; + +public class CloudLayouter(ICloudForm cloudForm) : ICloudLayouter +{ + public readonly List Rectangles = new(); + + public Rectangle PutNextRectangle(Size rectangleSize) + { + Rectangle rectangle; + + do + { + var point = cloudForm.GetNextPoint(); + point.Offset(-rectangleSize.Width / 2, -rectangleSize.Height / 2); + rectangle = new Rectangle(point, rectangleSize); + } while (Rectangles.Any(r => r.IntersectsWith(rectangle))); + + Rectangles.Add(rectangle); + return rectangle; + } +} \ No newline at end of file diff --git a/TagCloud2/CloudLayout/ICloudLayouter.cs b/TagCloud2/CloudLayout/ICloudLayouter.cs new file mode 100644 index 000000000..1851a0eec --- /dev/null +++ b/TagCloud2/CloudLayout/ICloudLayouter.cs @@ -0,0 +1,8 @@ +using System.Drawing; + +namespace TagCloud2.CloudLayout; + +public interface ICloudLayouter +{ + public Rectangle PutNextRectangle(Size rectangleSize); +} \ No newline at end of file diff --git a/TagCloud2/ColoringAlgorithms/GradientColor.cs b/TagCloud2/ColoringAlgorithms/GradientColor.cs new file mode 100644 index 000000000..40c6b6ec0 --- /dev/null +++ b/TagCloud2/ColoringAlgorithms/GradientColor.cs @@ -0,0 +1,23 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloud2.ColoringAlgorithms; + +public class GradientColor(Color gradientFrom, Color gradientTo) : IColorAlgorithm +{ + public Result GetColors(int count) + { + if (!gradientFrom.IsKnownColor || !gradientTo.IsKnownColor) + { + return Result.Fail("Gradient colors are unknown"); + } + + return Enumerable.Range(0, count) + .Select(i => Color.FromArgb( + (int) (gradientFrom.R + (gradientTo.R - gradientFrom.R) * (float) i / (count - 1)), + (int) (gradientFrom.G + (gradientTo.G - gradientFrom.G) * (float) i / (count - 1)), + (int) (gradientFrom.B + (gradientTo.B - gradientFrom.B) * (float) i / (count - 1)) + )) + .ToArray(); + } +} \ No newline at end of file diff --git a/TagCloud2/ColoringAlgorithms/IColorAlgorithm.cs b/TagCloud2/ColoringAlgorithms/IColorAlgorithm.cs new file mode 100644 index 000000000..699efe9a9 --- /dev/null +++ b/TagCloud2/ColoringAlgorithms/IColorAlgorithm.cs @@ -0,0 +1,9 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloud2.ColoringAlgorithms; + +public interface IColorAlgorithm +{ + public Result GetColors(int count); +} \ No newline at end of file diff --git a/TagCloud2/ColoringAlgorithms/RandomColor.cs b/TagCloud2/ColoringAlgorithms/RandomColor.cs new file mode 100644 index 000000000..ccf5f15a2 --- /dev/null +++ b/TagCloud2/ColoringAlgorithms/RandomColor.cs @@ -0,0 +1,19 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloud2.ColoringAlgorithms; + +public class RandomColor : IColorAlgorithm +{ + public Result GetColors(int count) + { + Random random = new Random(); + + return Enumerable.Range(0, count) + .Select(_ => Color.FromArgb( + random.Next(256), + random.Next(256), + random.Next(256))) + .ToArray(); + } +} \ No newline at end of file diff --git a/TagCloud2/ColoringAlgorithms/SingleColor.cs b/TagCloud2/ColoringAlgorithms/SingleColor.cs new file mode 100644 index 000000000..a40e56e4e --- /dev/null +++ b/TagCloud2/ColoringAlgorithms/SingleColor.cs @@ -0,0 +1,14 @@ +using System.Drawing; +using TagCloud; + +namespace TagCloud2.ColoringAlgorithms; + +public class SingleColor(Color color) : IColorAlgorithm +{ + public Result GetColors(int count) + { + return !color.IsKnownColor + ? Result.Fail("Unknown color") + : Enumerable.Repeat(color, count).ToArray(); + } +} \ No newline at end of file diff --git a/TagCloud2/Drawer/DrawerSettings.cs b/TagCloud2/Drawer/DrawerSettings.cs new file mode 100644 index 000000000..0b9204b3b --- /dev/null +++ b/TagCloud2/Drawer/DrawerSettings.cs @@ -0,0 +1,6 @@ +using System.Drawing; +using TagCloud2.ColoringAlgorithms; + +namespace TagCloud2.Drawer; + +public record DrawerSettings(IColorAlgorithm WordsColor, Size CloudSize, string Font); \ No newline at end of file diff --git a/TagCloud2/Drawer/ICloudDrawer.cs b/TagCloud2/Drawer/ICloudDrawer.cs new file mode 100644 index 000000000..ad5e38a90 --- /dev/null +++ b/TagCloud2/Drawer/ICloudDrawer.cs @@ -0,0 +1,8 @@ +using TagCloud; + +namespace TagCloud2.Drawer; + +public interface ICloudDrawer +{ + public Result DrawTagsCloudFromFile(string filepath); +} \ No newline at end of file diff --git a/TagCloud2/Drawer/RectanglesCloudDrawer.cs b/TagCloud2/Drawer/RectanglesCloudDrawer.cs new file mode 100644 index 000000000..c89ae46f7 --- /dev/null +++ b/TagCloud2/Drawer/RectanglesCloudDrawer.cs @@ -0,0 +1,68 @@ +using System.Drawing; +using TagCloud; +using TagCloud.TextPreparator; +using TagCloud2.CloudGenerator; +using TagCloud2.CloudLayout; +using Color = System.Drawing.Color; +using Font = System.Drawing.Font; +using Size = System.Drawing.Size; + + +namespace TagCloud2.Drawer; + +public class RectanglesCloudDrawer( + DrawerSettings drawerSettings, + IWordsFrequency wordsFrequency, + IRectanglesGenerator rectanglesGenerator) + : ICloudDrawer +{ + private Result DrawCloud(IList words, Size size) + { + var bmp = new Bitmap(size.Width, size.Height); + using var graphics = Graphics.FromImage(bmp); + + var bgColor = Color.White; + graphics.Clear(bgColor); + var colors = drawerSettings.WordsColor.GetColors(words.Count); + var colorsValue = colors.Value; + if (!colors.IsSuccess) + { + return Result.Fail(colors.Error); + } + + var rectBrush = new SolidBrush(bgColor); + var i = 0; + var fontFamilies = FontFamily.Families; + var isFontExist = fontFamilies.Any(f => + f.Name.Equals(drawerSettings.Font, StringComparison.OrdinalIgnoreCase)); + if (!isFontExist) + { + return Result.Fail("Current font doesn't exist"); + } + + foreach (var (word, rect, fontSize) in words) + { + var textBrush = new SolidBrush(colorsValue[i++]); + graphics.FillRectangle(rectBrush, rect); + var font = new Font(drawerSettings.Font, fontSize); + + var stringFormat = new StringFormat() + { + Alignment = StringAlignment.Center, + LineAlignment = StringAlignment.Center + }; + + graphics.DrawString(word, font, textBrush, rect, stringFormat); + } + + return bmp; + } + + public Result DrawTagsCloudFromFile(string filepath) + { + return wordsFrequency.GetWordsFrequencyFromFile(filepath) + .Then(word => rectanglesGenerator.GetWordsInShape(word)) + .Then(wordsInShape => DrawCloud(wordsInShape, wordsInShape.GetCloudSize())) + .Then(result => SaviorImages.SaveImage(result, "TagCloud", "PNG")); + } +} \ No newline at end of file diff --git a/TagCloud2/FileReader/IFileReader.cs b/TagCloud2/FileReader/IFileReader.cs new file mode 100644 index 000000000..c949ea5c5 --- /dev/null +++ b/TagCloud2/FileReader/IFileReader.cs @@ -0,0 +1,8 @@ +using TagCloud; + +namespace TagCloud2.FileReader; + +public interface IFileReader +{ + public Result> TryReadFile(string filePath); +} \ No newline at end of file diff --git a/TagCloud2/FileReader/TxtReader.cs b/TagCloud2/FileReader/TxtReader.cs new file mode 100644 index 000000000..5c3039451 --- /dev/null +++ b/TagCloud2/FileReader/TxtReader.cs @@ -0,0 +1,28 @@ +using TagCloud; + +namespace TagCloud2.FileReader; + +public class TxtReader : IFileReader +{ + public Result> TryReadFile(string filePath) + { + try + { + File.ReadAllText(filePath); + } + catch (FileNotFoundException) + { + return Result.Fail>($"File not found: {filePath}"); + } + catch (UnauthorizedAccessException) + { + return Result.Fail>($"Access to the file is denied: {filePath}"); + } + catch (IOException ex) + { + return Result.Fail>($"Error reading file: {ex.Message}"); + } + + return Result.Ok(File.ReadLines(filePath)); + } +} \ No newline at end of file diff --git a/TagCloud2/Result.cs b/TagCloud2/Result.cs new file mode 100644 index 000000000..e3b0e7b74 --- /dev/null +++ b/TagCloud2/Result.cs @@ -0,0 +1,130 @@ +namespace TagCloud2; + +public class None +{ + private None() + { + } +} + +public readonly struct Result +{ + public string Error { get; } + public T Value { get; } + + public Result(string error, T value = default!) + { + Error = error; + Value = value; + } + + public static implicit operator Result(T v) + { + return Result.Ok(v); + } + + public T GetValueOrThrow() + { + if (IsSuccess) + return Value; + throw new InvalidOperationException($"No value. Only Error {Error}"); + } + + public bool IsSuccess => Error is 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, + 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 ReplaceError( + this Result input, + Func replaceError) + { + return input.IsSuccess + ? input + : Fail(replaceError(input.Error)); + } + + public static Result RefineError( + this Result input, + string errorMessage) + { + return input.ReplaceError(err => errorMessage + ". " + err); + } +} \ No newline at end of file diff --git a/TagCloud2/SaviorImages.cs b/TagCloud2/SaviorImages.cs new file mode 100644 index 000000000..d5a960baf --- /dev/null +++ b/TagCloud2/SaviorImages.cs @@ -0,0 +1,12 @@ +using System.Drawing; + +namespace TagCloud2; + +public static class SaviorImages +{ + public static void SaveImage(Bitmap bmp, string fileName, string imageFormat) + { + fileName = $"{fileName}.{imageFormat.ToLower()}"; + bmp.Save(fileName); + } +} \ No newline at end of file diff --git a/TagCloud2/TagCloud2.csproj b/TagCloud2/TagCloud2.csproj new file mode 100644 index 000000000..58613f02c --- /dev/null +++ b/TagCloud2/TagCloud2.csproj @@ -0,0 +1,16 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + diff --git a/TagCloud2/TagCloudApp/Options.cs b/TagCloud2/TagCloudApp/Options.cs new file mode 100644 index 000000000..8d7922622 --- /dev/null +++ b/TagCloud2/TagCloudApp/Options.cs @@ -0,0 +1,24 @@ +using CommandLine; + +namespace TagCloud2.TagCloudApp; + +public class Options +{ + [Option('f', "file", Required = true, HelpText = "Путь к исходному текстовому файлу.")] + public string FilePath { get; set; } + + [Option('a', "algorithm", Default = TagCloudAlgorithms.Circular, + HelpText = "Алгоритм построение облака (Circular или Square)")] + public TagCloudAlgorithms TagCloudAlgorithm { get; set; } + + [Option('w', "width", Default = 1000, HelpText = "Размер облака тегов")] + public int Size { get; set; } + + [Option('c', "WordsColor", Default = "random", HelpText = "Алгоритм расцветки " + + "единый цвет для всех слов, random или " + + "градиент - указать два цвета (от-до)")] + public string WordsColor { get; set; } + + [Option('t', "font", Default = "Times New Roman", HelpText = "Шрифт для текста.")] + public string Font { get; set; } +} \ No newline at end of file diff --git a/TagCloud2/TagCloudApp/Program.cs b/TagCloud2/TagCloudApp/Program.cs new file mode 100644 index 000000000..3d4dd4067 --- /dev/null +++ b/TagCloud2/TagCloudApp/Program.cs @@ -0,0 +1,89 @@ +using System.Drawing; +using Autofac; +using CommandLine; +using TagCloud.TextPreparator; +using TagCloud2.CloudForms; +using TagCloud2.CloudGenerator; +using TagCloud2.CloudLayout; +using TagCloud2.ColoringAlgorithms; +using TagCloud2.Drawer; +using TagCloud2.FileReader; + +namespace TagCloud2.TagCloudApp; + +public static class Program +{ + public static void Main(string[] args) + { + try + { + Parser.Default.ParseArguments(args) + .WithParsed(options => + { + var container = ConfigureContainer(options); + container.Resolve(); + var generator = container.Resolve(); + var result = generator.DrawTagsCloudFromFile(options.FilePath); + Console.WriteLine(result.IsSuccess + ? $"Путь к файлу: {options.FilePath} " + Environment.NewLine + + $"Алгоритм: {options.TagCloudAlgorithm}" + Environment.NewLine + + $"Алгоритм расцветки слов: {options.WordsColor}" + Environment.NewLine + + $"Шрифт: {options.Font}" + Environment.NewLine + + $"Размер: {options.Size}" + : $"Failed to save image : {result.Error}"); + }) + .WithNotParsed(errors => { Console.WriteLine("Error in command line parameters."); }); + } + catch (Exception e) + { + Console.WriteLine(e); + } + } + + private static IContainer ConfigureContainer(Options options) + { + var builder = new ContainerBuilder(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.Register(BuildColoringAlgorithm(options)); + builder.Register(BuildDrawerSettings(options)); + builder.Register(BuildCloudForm(options)); + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterType().As(); + return builder.Build(); + } + + private static Func BuildColoringAlgorithm(Options options) + { + if (options.WordsColor.ToLower().Equals("random")) + { + return c => new RandomColor(); + } + + var gradient = options.WordsColor.Split("-").Select(Color.FromName).ToArray(); + if (gradient.Length == 1) + { + return c => new SingleColor(Color.FromName(options.WordsColor)); + } + + return c => new GradientColor(gradient[0], gradient[1]); + } + + private static Func BuildDrawerSettings(Options options) => + c => new DrawerSettings( + c.Resolve(), + new Size(options.Size, options.Size), + options.Font); + + private static Func BuildCloudForm(Options options) + { + return options.TagCloudAlgorithm switch + { + TagCloudAlgorithms.Circular => _ => new CircularSpiral(new Point(options.Size / 2, options.Size / 2)), + TagCloudAlgorithms.Square => _ => new SquareSpiral(new Point(options.Size / 2, options.Size / 2)), + _ => _ => new CircularSpiral(new Point(options.Size / 2, options.Size / 2)) + }; + } +} \ No newline at end of file diff --git a/TagCloud2/TagCloudApp/TagCloudAlgorithms.cs b/TagCloud2/TagCloudApp/TagCloudAlgorithms.cs new file mode 100644 index 000000000..a91666c6a --- /dev/null +++ b/TagCloud2/TagCloudApp/TagCloudAlgorithms.cs @@ -0,0 +1,7 @@ +namespace TagCloud2.TagCloudApp; + +public enum TagCloudAlgorithms +{ + Circular, + Square +} \ No newline at end of file diff --git a/TagCloud2/TextPreparator/ITextFilter.cs b/TagCloud2/TextPreparator/ITextFilter.cs new file mode 100644 index 000000000..e0638f51f --- /dev/null +++ b/TagCloud2/TextPreparator/ITextFilter.cs @@ -0,0 +1,8 @@ +using TagCloud2; + +namespace TagCloud.TextPreparator; + +public interface ITextFilter +{ + public Result> GetFilteredText(IEnumerable words); +} \ No newline at end of file diff --git a/TagCloud2/TextPreparator/IWordsFrequency.cs b/TagCloud2/TextPreparator/IWordsFrequency.cs new file mode 100644 index 000000000..25336dd52 --- /dev/null +++ b/TagCloud2/TextPreparator/IWordsFrequency.cs @@ -0,0 +1,8 @@ +using TagCloud2; + +namespace TagCloud.TextPreparator; + +public interface IWordsFrequency +{ + public Result> GetWordsFrequencyFromFile(string fileName); +} \ No newline at end of file diff --git a/TagCloud2/TextPreparator/TextFilter.cs b/TagCloud2/TextPreparator/TextFilter.cs new file mode 100644 index 000000000..b0f708e8f --- /dev/null +++ b/TagCloud2/TextPreparator/TextFilter.cs @@ -0,0 +1,22 @@ +using TagCloud2; + +namespace TagCloud.TextPreparator; + +public class TextFilter : ITextFilter +{ + private static readonly HashSet BoringWords = new() + { + "а", "и", "в", "на", "с", "по", "для", "о", "как", "к", "из", "когда", "что", "но", "не", "бы", "же", "только", + "из-за", "из-под", "около", "вокруг", "перед", "возле", + "он", "она", "оно", "они", "им", "ей", "ему", "её", "его", "их" + }; + + public Result> GetFilteredText(IEnumerable words) + { + return Result.Of(() =>words + .Select(s => s.Trim()) + .Select(s => s.ToLower()) + .Where(line => !string.IsNullOrWhiteSpace(line)) + .Where(word => !BoringWords.Contains(word))); + } +} \ No newline at end of file diff --git a/TagCloud2/TextPreparator/TextHandler.cs b/TagCloud2/TextPreparator/TextHandler.cs new file mode 100644 index 000000000..e901a58d3 --- /dev/null +++ b/TagCloud2/TextPreparator/TextHandler.cs @@ -0,0 +1,27 @@ +using TagCloud2; +using TagCloud2.FileReader; + +namespace TagCloud.TextPreparator; + +public class TextHandler(ITextFilter textFilter, IFileReader fileReader) : IWordsFrequency +{ + private readonly Dictionary _wordCount = new(); + + private Result> GetWordsFrequency(IEnumerable words) + { + foreach (var word in words) + { + if (!_wordCount.TryAdd(word, 1)) + _wordCount[word]++; + } + + return _wordCount; + } + + public Result> GetWordsFrequencyFromFile(string fileName) + { + return fileReader.TryReadFile(fileName) + .Then(words => textFilter.GetFilteredText(words)) + .Then(filteredWords => GetWordsFrequency(filteredWords)); + } +} \ No newline at end of file diff --git a/TagCloud2Tests/CloudGeneratorTests.cs b/TagCloud2Tests/CloudGeneratorTests.cs new file mode 100644 index 000000000..ade3a5c3e --- /dev/null +++ b/TagCloud2Tests/CloudGeneratorTests.cs @@ -0,0 +1,76 @@ +using System.Drawing; +using FakeItEasy; +using FluentAssertions; +using NUnit.Framework; +using TagCloud2.CloudGenerator; +using TagCloud2.CloudLayout; +using TagCloud2.ColoringAlgorithms; +using TagCloud2.Drawer; + +namespace TagCloud2Tests; + +public class CloudGeneratorTests +{ + private ICloudLayouter cloudLayouter; + private IColorAlgorithm colorAlgorithm; + private DrawerSettings drawerSettings; + private RectanglesGenerator rectanglesGenerator; + private static readonly Rectangle Rectangle = new(0, 0, 10, 10); + + [SetUp] + public void Setup() + { + cloudLayouter = A.Fake(); + colorAlgorithm = A.Fake(); + drawerSettings = new DrawerSettings(colorAlgorithm, new Size(100, 100), "Georgia"); + rectanglesGenerator = new RectanglesGenerator(cloudLayouter, drawerSettings); + } + + [Test] + public void GetWordsInShape_ShouldReturnCorrectResultSize() + { + A.CallTo(() => cloudLayouter.PutNextRectangle(new Size(200, 33))) + .Returns(new Rectangle(0, 0, 50, 50)); + A.CallTo(() => cloudLayouter.PutNextRectangle(new Size(333, 66))) + .Returns(new Rectangle(0, 0, 50, 50)); + A.CallTo(() => cloudLayouter.PutNextRectangle(new Size(700, 100))) + .Returns(new Rectangle(0, 0, 50, 50)); + var dict = new Dictionary + { + {"собака", 1}, + {"кошка", 2}, + {"попугай", 3} + }; + var expected = 3; + + var actual = rectanglesGenerator.GetWordsInShape(dict); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Count.Should().Be(expected); + } + + [Test] + public void GetWordsInShape_ShouldReturnCorrectResult() + { + A.CallTo(() => cloudLayouter.PutNextRectangle(new Size(300, 50))) + .Returns(Rectangle); + A.CallTo(() => cloudLayouter.PutNextRectangle(new Size(500, 100))) + .Returns(Rectangle); + + var words = new Dictionary + { + {"собака", 1}, + {"кошка", 2}, + }; + var expected = new List + { + new("собака", Rectangle, 1), + new("кошка", Rectangle, 2), + }; + + var actual = rectanglesGenerator.GetWordsInShape(words); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file diff --git a/TagCloud2Tests/CloudLayoutTests.cs b/TagCloud2Tests/CloudLayoutTests.cs new file mode 100644 index 000000000..f2fecf1b0 --- /dev/null +++ b/TagCloud2Tests/CloudLayoutTests.cs @@ -0,0 +1,120 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagCloud2; +using TagCloud2.CloudForms; +using TagCloud2.CloudLayout; + +namespace TagCloud2Tests; + +public class CloudLayoutTests +{ + private static CloudLayouter _cloudLayouter; + private static ICloudForm _cloudForm; + + + [SetUp] + public void SetUp() + { + _cloudForm = new CircularSpiral(new Point(500, 500)); + _cloudLayouter = new CloudLayouter(_cloudForm); + } + + [Test] + public void PutNextRectangle_ShouldAddNewRectangle() + { + var size = new Size(10, 20); + var coordinateX = 500 - size.Width / 2; + var coordinateY = 500 - size.Height / 2; + var expected = new Rectangle(coordinateX, coordinateY, size.Width, size.Height); + + _cloudLayouter.PutNextRectangle(size); + + _cloudLayouter.Rectangles.Should().Contain(expected); + } + + [Test] + public void CloudLayouter_RectAngelsListShouldHaveCorrectSize() + { + _cloudLayouter.PutNextRectangle(new Size(40, 20)); + _cloudLayouter.PutNextRectangle(new Size(20, 40)); + _cloudLayouter.PutNextRectangle(new Size(50, 50)); + + _cloudLayouter.Rectangles.Count.Should().Be(3); + } + + [Test] + public void CloudLayouter_RectAngelsShouldNoIntersectsWithOthers() + { + Random rnd = new Random(); + for (int i = 0; i < 100; i++) + { + int width = rnd.Next(15, 40); + int height = rnd.Next(15, 40); + _cloudLayouter.PutNextRectangle(new Size(width, height)); + } + + List rectangels = _cloudLayouter.Rectangles; + + foreach (Rectangle rectangle in rectangels) + { + rectangels.Where((_, j) => j != rectangels.IndexOf(rectangle)) + .All(r => !r.IntersectsWith(rectangle)) + .Should().BeTrue(); + } + } + + [Test] + public void GetNextPoint_ShouldReturnPointsOnSpiral_WithCircularSpiral() + { + var spiral = new CircularSpiral(new Point(500, 500)); + var expected = new Point[] + { + new(500, 500), new(500, 500), new(499, 501), new(497, 500), new(497, 496), + new(501, 495), new(505, 498) + }; + + var actual = new List(); + for (int i = 0; i < 700; i++) + { + var point = spiral.GetNextPoint(); + if (i % 100 == 0) + actual.Add(point); + } + + actual.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetNextPoint_ShouldReturnPointsOnSpiral_WithSquareSpiral() + { + var spiral = new SquareSpiral(new Point(500, 500)); + var expected = new Point[] + { + new(500, 501), new(501, 501), new(501, 500), + new(501, 499), new(500, 499), new(499, 499), new(499, 500), + }; + + var actual = new List(); + for (int i = 0; i < 7; i++) + { + var point = spiral.GetNextPoint(); + actual.Add(point); + } + + actual.Should().BeEquivalentTo(expected); + } + + + [Test, MaxTime(5000)] + public void CloudLayouter_ShouldWorkInTime() + { + Random rnd = new Random(); + for (int i = 0; i < 10000; i++) + { + int width = rnd.Next(1, 10); + int height = rnd.Next(1, 10); + _cloudLayouter.PutNextRectangle(new Size(width, height)); + } + } +} \ No newline at end of file diff --git a/TagCloud2Tests/ColoringAlgorithmsTests.cs b/TagCloud2Tests/ColoringAlgorithmsTests.cs new file mode 100644 index 000000000..5fe139eac --- /dev/null +++ b/TagCloud2Tests/ColoringAlgorithmsTests.cs @@ -0,0 +1,72 @@ +using System.Drawing; +using FluentAssertions; +using NUnit.Framework; +using TagCloud2.ColoringAlgorithms; + +namespace TagCloud2Tests; + +public class ColoringAlgorithmsTests +{ + private readonly GradientColor _gradientColor = new(Color.Red, Color.Blue); + private readonly RandomColor _randomColor = new(); + private readonly SingleColor _singleColor = new(Color.Green); + + [Test] + public void GetColors_ShouldReturnIdenticalColors_WithGradientAlgorithm() + { + var expected = new[] + { + Color.FromArgb(255, 0, 0), + Color.FromArgb(0, 0, 255) + }; + + var actual = _gradientColor.GetColors(2); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetColors_ShouldReturnCorrectSize_WithGradientAlgorithm() + { + var expected = 267; + + var actual = _gradientColor.GetColors(267); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Length.Should().Be(expected); + } + + [Test] + public void GetColors_ShouldReturnCorrectSize_WhithRandomAlgorithm() + { + var expected = 267; + + var actual = _randomColor.GetColors(267); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Length.Should().Be(expected); + } + + [Test] + public void GetColors_ShouldReturnCorrectSize_WhithSingleAlgorithm() + { + var expected = 267; + + var actual = _singleColor.GetColors(267); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Length.Should().Be(expected); + } + + [Test] + public void GetColors_ShouldReturnCorrectColors_WhithSingleAlgorithm() + { + var expected = Color.Green; + + var actual = _singleColor.GetColors(50); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.All(color => color.Equals(expected)).Should().BeTrue(); + } +} \ No newline at end of file diff --git a/TagCloud2Tests/TagCloud2Tests.csproj b/TagCloud2Tests/TagCloud2Tests.csproj new file mode 100644 index 000000000..379d1d03f --- /dev/null +++ b/TagCloud2Tests/TagCloud2Tests.csproj @@ -0,0 +1,21 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/TagCloud2Tests/TextPreparatorTests.cs b/TagCloud2Tests/TextPreparatorTests.cs new file mode 100644 index 000000000..97faf1529 --- /dev/null +++ b/TagCloud2Tests/TextPreparatorTests.cs @@ -0,0 +1,126 @@ +using FluentAssertions; +using NUnit.Framework; +using TagCloud.TextPreparator; +using TagCloud2.FileReader; +using static System.IO.File; + +namespace TagCloud2Tests; + +public class TextPreparatorTests +{ + private const string TempFileName = "temp.txt"; + private static readonly TxtReader TxtReader = new(); + private static readonly TextFilter TextFilter = new(); + private static readonly TextHandler TextHandler = new TextHandler(TextFilter, TxtReader); + + [TearDown] + public void TearDown() + { + if (Exists(TempFileName)) + { + Delete(TempFileName); + } + } + + [Test] + public void TryReadFile_ShouldThrowException_WithNotExistedFile() + { + var invalidFilePath = "NotExistedfile.txt"; + + var actual = TxtReader.TryReadFile(invalidFilePath); + + actual.IsSuccess.Should().BeFalse(); + actual.Error.Should().Be($"File not found: {invalidFilePath}"); + } + + [Test] + public void TryReadFile_ShouldReduceToLowerCase_WhenReadingFile() + { + var words = new[] + { + "Рим", + "Сава", + "ШлЯпА" + }; + WriteAllLines(TempFileName, words); + var expected = new[] + { + "рим", + "сава", + "шляпа" + }; + + + var text = TxtReader.TryReadFile(TempFileName); + var actual = TextFilter.GetFilteredText(text.Value); + + actual.Value.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetFilteredText_ShouldIgnoreSpaces_WhenReadingFile() + { + var words = new[] + { + " кот", + "стены ", + " грыз ", + " ", + "" + }; + WriteAllLines(TempFileName, words); + var expected = new[] + { + "кот", + "стены", + "грыз" + }; + + + var text = TxtReader.TryReadFile(TempFileName); + var actual = TextFilter.GetFilteredText(text.Value); + + actual.Value.Should().BeEquivalentTo(expected); + } + + [Test] + public void GetFilteredText_ShouldRemove_WhenTextContainsBoringWords() + { + var words = new List {"когда", "собака", "гуляет", "на"}; + var expected = new List {"собака", "гуляет"}; + + var actual = TextFilter.GetFilteredText(words); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.ToList().Should().BeEquivalentTo(expected); + } + + [Test] + public void GetFilteredText_ShouldReturnSameText_WhenNoBoringWords() + { + var expected = new List {"собака", "гуляет"}; + + var actual = TextFilter.GetFilteredText(expected); + + + actual.IsSuccess.Should().BeTrue(); + actual.Value.ToList().Should().BeEquivalentTo(expected); + } + + [Test] + public void HandleText_ShouldReturnCorrectWordFrenquency() + { + var words = new[] {"собака", "гуляет", "собака"}; + WriteAllLines(TempFileName, words); + var expected = new Dictionary + { + {"собака", 2}, + {"гуляет", 1} + }; + + var actual = TextHandler.GetWordsFrequencyFromFile(TempFileName); + + actual.IsSuccess.Should().BeTrue(); + actual.Value.Should().BeEquivalentTo(expected); + } +} \ No newline at end of file